quick-cocos2d-x的热更新机制实现

0 依赖

这里说的热更新,指的是客户端的更新。

大致的流程是,客户端在启动后访问更新api,根据更新api的反馈,下载更新资源,然后使用新的资源启动客户端,或者直接使用新资源不重启客户端。

这种方式可以跳过AppStore的审核,避免了用户频繁下载、安装、覆盖产品包。

我们一般使用这种方式快速修复产品BUG和增加新功能。

本文基于 quick-cocos2d-x zrong 修改版

1 前言

1.1 他山之石

在实现这个机制之前,我研究了这几篇文章:

另外,我也查看了 AssetsManager 的源码和 sample

不幸的是,这几个方案我都不能直接拿来用。因此思考再三,还是自己写了一套方案。

==重要提醒==

这篇文章很长,但我不愿意将其分成多个部分。这本来就是一件事,分开的话有种开房时洗完澡妹子却说两个小时后才能来。这中间干点啥呢?

所以,如果你不能坚持两个小时(能么?不能?),或者你的持久度不能坚持到把这篇文章看完(大概要10~30分钟吧),那还是不要往下看的比较好。

当然,你也可能坚挺了30分钟之后才发现妹子是凤姐,不要怪我这30分钟里面没开灯哦……

1.2 为什么要重复造轮子

上面的几个方案侧重于尽量简化用户(使用此方案的程序员)的操作,而简化带来的副作用就是会损失一些灵活性。

正如 Roberto Ierusalimschy 在 Lua程序设计(第2版) 第15章开头所说:

通常,Lua不会设置规则(policy)。相反,Lua会提供许多强有力的机制来使开发者有能力实现出最适合的规则。

我认为更新模块也不应该设置规则,而是尽可能提供一些机制来满足程序员的需要。这些机制并不是我发明的,而是Lua和quick本来就提供的。让程序员自己实现自己的升级系统,一定比我这个无证野路子的方法更好.

因此,本文中讲述的并非是一套通用的机制,而是我根据上面说到的这些机制实现的一套适合自己的方法。当然你可以直接拿去用,但要记住:

  • 用的好,请告诉你的朋友。
  • 出了问题,请告诉别找我。

1.3 需求的复杂性

热更新有许多的必要条件,每个产品的需求可能都不太相同。

例如,每个产品的版本号设计都不太相同,有的有大版本、小版本;有的则有主版本、次版本、编译版本。我以前的习惯,是在主版本变化的时候需要整包更新,而次版本变化代表逻辑更新,编译版本代表资源更新等等。这些需要自己来定义升级规则。

再例如,有的产品希望逐个下载升级包,有的产品希望把所有资源打包成一个升级包;有的产品直接使用文件名作为资源名在游戏中调用,而有的产品会把资源名改为指纹码(例如MD5)形式来实现升级的多版本共存和实时回滚,还有的产品甚至要求能在用户玩游戏的过程中完成自动更新。

AssetsManager 那套机制就太死板,在真实的产品中不修改很难使用。

而我也不建议使用 CCUserDefault 这种东西——在Lua的世界里,为什么要用XML做配置文件?

如果抽象出我的需求,其实只有1点:

能更新一切

这个说的有点大了,准确的说,应该是 能更新一切Lua代码与资源

如果你的整个游戏都是Lua写的(对于quick的项目来说应该是这样),其实也就是更新一切。

1.4 版本号设计

关于上面 需求的复杂性 中提到的版本号的问题,可以参考一下这篇文章:语义化版本2.0.0

我基于语义化版本设计了一套规则在团队内部使用:项目版本描述规则

在这里,我尽量详细地阐述我的思路和做法,抛砖引玉吧。

2 特色

基本的热更新功能就不说了大家都有。我这套机制还有如下几个特色:

2.1 可以更新 frameworks_precompiled.zip 模块

为了行文方便,后面会把 frameworks_precompiled.zip 简称为 framework

frameworks 模块是 quick 的核心模块,在quick 生成的项目中,它直接在 AppDelegate.cpp 中载入 main.lua 之前进行载入。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool AppDelegate::applicationDidFinishLaunching()
{
// initialize director
CCDirector *pDirector = CCDirector::sharedDirector();
pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
pDirector->setProjection(kCCDirectorProjection2D);

// set FPS. the default value is 1.0/60 if you don't call this
pDirector->setAnimationInterval(1.0 / 60);

// register lua engine
CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);

CCLuaStack *pStack = pEngine->getLuaStack();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
// load framework
pStack->loadChunksFromZIP("res/framework_precompiled.zip");

// set script path
string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("scripts/main.lua");
......

这可以说明这个核心模块对quick的重要性。正因为它重要,所以必须要能更新它。

2.2 可以更新 update 模块自身

更新功能是客户端启动后载入的第一个lua模块,它负责载入更新资源,以及启动主项目。一般情况下,这个模块是不需要改动的。对它进行改动,既不科学,也不安全(安全啊……)。

但是万一呢?大家知道策划和运营同学都是二班的,或许哪天就有二班同学找你说:改改怕什么?又不会怀孕…… 所以这个必须有。

2.3 纯lua实现

把这个拿出来说纯粹是撑数的。不凑个三大特色怎么好意思?

上面SunLightJuly和Henry同学的方案当然也是纯lua的。用quick你不搞纯lua好意思出来混?小心廖大一眼瞪死你。

当然,我这个不是纯lua的,我基于 AssetsManager(C++) 的代码实现了一个 Updater 模块。

而且,我还改了 AppDelegate 中的启动代码。

所以,你看,我不仅是撑数,还是忽悠。

3 Updater(C++)

AssetsManager 中提供了下载资源,访问更新列表,解压zip包,删除临时文件,设置搜索路径等等一系列的功能。但它的使用方式相当死板,我只能传递一个获取版本号的地址,一个zip包的地址,一个临时文件夹路径,然后就干等着。期间啥也干不了。

当然,我可以通过 quick 为其增加的 registerScriptHandler 方法让lua得知下载进度和网络状态等等。但下载进度的数字居然以事件名的方式通过字符串传过来的!这个就太匪夷所思了点。

于是,我对这个 AssetsManager 进行了修改。因为修改的东西实在太多,改完后就不好意思再叫这个名字了(其实主要是现在的名字比较短 XD)。我们只需要记住这个 Updater 是使用 AssetsManager 修改的即可。

在上面SunLightJuly和Henry同学的方法中,使用的是 CCHTTPRequest 来获取网络资源的。CCHTTPRequest 封装了cURL 操作。而在 Updater 中,是直接封装的 cURL 操作。

在我的设计中,逻辑应该尽量放在lua中,C++部分只提供功能供lua调用。因为lua可以进行热更新,而C++部分则只能整包更新。

Updater 主要实现的内容如下:

3.1 删除了不需要的方法

get和set相关的一堆方法都被删除了。new对象的时候也不必传递参数了。

3.2 增加 getUpdateInfo 方法

这个方法通过HTTP协议获取升级列表数据,获取到的数据直接返回,C++并不做处理。

3.3 修改 update 方法

这个方法通过HTTP协议下载升级包,需要提供四个参数:

  1. zip文件的url;
  2. zip文件的保存位置;
  3. zip 文件的解压临时目录;
  4. 解压之前是否需要清空临时目录。

3.4 修改事件类型

我把把传递给lua的事件分成了四种类型:

3.4.1 UPDATER_MESSAGE_UPDATE_SUCCEED

事件名为 success,代表更新成功,zip文件下载并解压完毕;

3.4.2 UPDATER_MESSAGE_STATE

事件名为 state,更新过程中的状态(下载开始、结束,解压开始、结束)也传递给了lua。这个方法是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void Updater::Helper::handlerState(Message *msg)
{
StateMessage* stateMsg = (StateMessage*)msg->obj;
if(stateMsg->manager->_delegate)
{
stateMsg->manager->_delegate->onState(stateMsg->code);
}
if (stateMsg->manager->_scriptHandler)
{
std::string stateMessage;
switch ((StateCode)stateMsg->code)
{
case kDownStart:
stateMessage = "downloadStart";
break;

case kDownDone:
stateMessage = "downloadDone";
break;

case kUncompressStart:
stateMessage = "uncompressStart";
break;
case kUncompressDone:
stateMessage = "uncompressDone";
break;

default:
stateMessage = "stateUnknown";
}

CCScriptEngineManager::sharedManager()
->getScriptEngine()
->executeEvent(
stateMsg->manager->_scriptHandler,
"state",
CCString::create(stateMessage.c_str()),
"CCString");
}

delete ((StateMessage*)msg->obj);
}

3.4.3 UPDATER_MESSAGE_PROGRESS

事件名为 progress,传递的对象为一个 CCInteger ,代表进度。详细的实现可以看 源码

3.4.4 UPDATER_MESSAGE_ERROR

事件名为 error,传递的对象是一个 CCString,值有这样几个:

  • errorCreateFile
  • errorNetwork
  • errorNoNewVersion
  • errorUncompress
  • errorUnknown

方法的实现和上面的 UPDATER_MESSAGE_STATE 类似,这里就不贴了。详细的实现可以看 源码

Updater(C++) 部分只做了这些苦力工作,而具体的分析逻辑(分析getUserInfo返回的数据决定是否升级、如何升级和升级什么),下载命令的发出(调用update方法),解压成功之后的操作(比如合并新文件到就文件中,更新文件索引列表等等),全部需要lua来做。下面是一个处理Updater(C++)事件的lua函数的例子。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function us._updateHandler(event, value)
updater.state = event
if event == "success" then
updater.stateValue = value:getCString()
-- 成功之后更新资源列表,合并新资源
updater.updateFinalResInfo()
-- 调用成功后的处理函数
if us._succHandler then
us._succHandler()
end
elseif event == "error" then
updater.stateValue = value:getCString()
elseif event == "progress" then
updater.stateValue = tostring(value:getValue())
elseif event == "state" then
updater.stateValue = value:getCString()
end
-- us._label 是一个CCLabelTTF,用来显示进度和状态
us._label:setString(updater.stateValue)
assert(event ~= "error",
string.format("Update error: %s !", updater.stateValue))
end

updater:registerScriptHandler(us._updateHandler)

4. update包(lua)

update包是整个项目的入口包,quick会首先载入这个包,甚至在 framework 之前。

4.1 为update包所做的项目修改

我修改了quick项目文件 AppDelegate.cpp 中的 applicationDidFinishLaunching 方法,使其变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
bool AppDelegate::applicationDidFinishLaunching()
{
// initialize director
CCDirector *pDirector = CCDirector::sharedDirector();
pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
pDirector->setProjection(kCCDirectorProjection2D);

// set FPS. the default value is 1.0/60 if you don't call this
pDirector->setAnimationInterval(1.0 / 60);

// register lua engine
CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);

CCLuaStack *pStack = pEngine->getLuaStack();

string gtrackback = "\
function __G__TRACKBACK__(errorMessage) \
print(\"----------------------------------------\") \
print(\"LUA ERROR: \" .. tostring(errorMessage) .. \"\\n\") \
print(debug.traceback(\"\", 2)) \
print(\"----------------------------------------\") \
end";
pEngine->executeString(gtrackback.c_str());

// load update framework
pStack->loadChunksFromZIP("res/lib/update.zip");

string start_path = "require(\"update.UpdateApp\").new(\"update\"):run(true)";
CCLOG("------------------------------------------------");
CCLOG("EXECUTE LUA STRING: %s", start_path.c_str());
CCLOG("------------------------------------------------");
pEngine->executeString(start_path.c_str());

return true;
}

原来位于 main.lua 中的 __G_TRACKBACK__ 函数(用于输出lua报错信息)直接包含在C++代码中了。那么现在 main.lua 就不再需要了。

同样的,第一个载入的模块变成了 res/lib/update.zip,这个zip也可以放在quick能找到的其它路径中,使用这个路径只是我的个人习惯。

最后,LuaStack直接执行了下面这句代码启动了 update.UpdateApp 模块:

1
require("update.UpdateApp").new("update"):run(true);

4.2 update包中的模块

update包有三个子模块,每个模块是一个lua文件,分别为:

  • update.UpdateApp 检测更新,决定启动哪个模块。
  • update.updater 负责真正的更新工作,与C++通信,下载、解压、复制。
  • update.updateScene 负责在更新过程中显示界面,进度条等等。

对于不同的大小写,是因为在我的命名规则中,类用大写开头,对象是小写开头。 update.UpdateApp 是一个类,其它两个是对象(table)。

下面的 4.3、4.4、4.5 将分别对这3个模块进行详细介绍。

4.3 update.UpdateApp

下面是入口模块 UpdateApp 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
--- The entry of Game
-- @author zrong(zengrong.net)
-- Creation 2014-07-03

local UpdateApp = {}

UpdateApp.__cname = "UpdateApp"
UpdateApp.__index = UpdateApp
UpdateApp.__ctype = 2

local sharedDirector = CCDirector:sharedDirector()
local sharedFileUtils = CCFileUtils:sharedFileUtils()
local updater = require("update.updater")

function UpdateApp.new(...)
local instance = setmetatable({}, UpdateApp)
instance.class = UpdateApp
instance:ctor(...)
return instance
end

function UpdateApp:ctor(appName, packageRoot)
self.name = appName
self.packageRoot = packageRoot or appName

print(string.format("UpdateApp.ctor, appName:%s, packageRoot:%s", appName, packageRoot))

-- set global app
_G[self.name] = self
end

function UpdateApp:run(checkNewUpdatePackage)
--print("I am new update package")
local newUpdatePackage = updater.hasNewUpdatePackage()
print(string.format("UpdateApp.run(%s), newUpdatePackage:%s",
checkNewUpdatePackage, newUpdatePackage))
if checkNewUpdatePackage and newUpdatePackage then
self:updateSelf(newUpdatePackage)
elseif updater.checkUpdate() then
self:runUpdateScene(function()
_G["finalRes"] = updater.getResCopy()
self:runRootScene()
end)
else
_G["finalRes"] = updater.getResCopy()
self:runRootScene()
end
end

-- Remove update package, load new update package and run it.
function UpdateApp:updateSelf(newUpdatePackage)
print("UpdateApp.updateSelf ", newUpdatePackage)
local updatePackage = {
"update.UpdateApp",
"update.updater",
"update.updateScene",
}
self:_printPackages("--before clean")
for __,v in ipairs(updatePackage) do
package.preload[v] = nil
package.loaded[v] = nil
end
self:_printPackages("--after clean")
_G["update"] = nil
CCLuaLoadChunksFromZIP(newUpdatePackage)
self:_printPackages("--after CCLuaLoadChunksForZIP")
require("update.UpdateApp").new("update"):run(false)
self:_printPackages("--after require and run")
end

-- Show a scene for update.
function UpdateApp:runUpdateScene(handler)
self:enterScene(require("update.updateScene").addListener(handler))
end

-- Load all of packages(except update package, it is not in finalRes.lib)
-- and run root app.
function UpdateApp:runRootScene()
for __, v in pairs(finalRes.lib) do
print("runRootScene:CCLuaLoadChunksFromZip",__, v)
CCLuaLoadChunksFromZIP(v)
end

require("root.RootScene").new("root"):run()
end

function UpdateApp:_printPackages(label)
label = label or ""
print("\npring packages "..label.."------------------")
for __k, __v in pairs(package.preload) do
print("package.preload:", __k, __v)
end
for __k, __v in pairs(package.loaded) do
print("package.loaded:", __k, __v)
end
print("print packages "..label.."------------------\n")
end


function UpdateApp:exit()
sharedDirector:endToLua()
os.exit()
end

function UpdateApp:enterScene(__scene)
if sharedDirector:getRunningScene() then
sharedDirector:replaceScene(__scene)
else
sharedDirector:runWithScene(__scene)
end
end

return UpdateApp

我来说几个重点。

4.3.1 没有framework

由于没有加载 framework,class当然是不能用的。所有quick framework 提供的方法都不能使用。

我借用class中的一些代码来实现 UpdateApp 的继承。其实我觉得这个UpdateApp也可以不必写成class的。

4.3.2 入口函数 update.UpdateApp:run(checkNewUpdatePackage)

run 是入口函数,同时接受一个参数,这个参数用于判断是否要检测本地有新的 update.zip 模块。

是的,run 就是那个在 AppDelegate.cpp 中第一个调用的lua函数。

这个函数接受一个参数 checkNewUpdatePackage ,在C++调用 run 的时候,传递的值是 true

如果这个值为真,则会检测本地是否拥有新的更新模块,这个检测通过 update.updater.hasNewUpdatePackage() 方法进行,后面会说到这个方法。

本地有更新的 update 模块,则直接调用 updateSelf 来更新 update 模块自身;若无则检测是否有项目更新,下载更新的资源,解析它们,处理它们,然后启动主项目。这些工作通过 update.updater.checkUpdate() 完成,后面会说到这个方法。

若没有任何内容需要更新,则直接调用 runRootScene 来显示主场景了。这后面的内容就交给住场景去做了,update 模块退出历史舞台。

从上面这个流程可以看出。在更新完成之前,主要的项目代码和资源没有进行任何载入。这也就大致达到了我们 更新一切 的需求。因为所有的东西都没有载入,也就不存在更新。只需要保证我载入的内容是最新的就行了。

因此,只要保证 update 模块能更新,就达到我们最开始的目标了。

这个流程还可以保证,如果没有更新,甚至根本就不需要载入 update 模块的场景界面,直接跳转到游戏的主场景即可。

有句代码在 run 函数中至关重要:

1
_G["finalRes"] = updater.getResCopy()

finalRes 这个全局变量保存了本地所有的 原始/更新 资源索引。它是一个嵌套的tabel,保存的是所有资源的名称以及它们对应的 绝对/相对 路径的对应关系。后面会详述。

4.3.3 更新自身 update.UpdateApp:updateSelf(newUpdatePackage)

这是本套机制中最重要的一环。理解了它,你就知道更新一切其实没什么秘密。Lua本来就提供了这样一套机制。

由于在 C++ 中已经将 update 模块载入了内存,那么要更新自身首先要做的是清除 Lua 的载入标记。

Lua在两个全局变量中做了标记:

  • package.preload 执行 CCLuaLoadChunksFromZIP 之后会将模块缓存在这里作为 require 的加载器;
  • package.loaded 执行 require 的时候会先查询 package.loaded,若没有则会查询 package.preload 得到加载器,利用加载器加载模块,再将加载的模块缓存到 package.loaded 中。

详细的机制可以阅读 Lua程序设计(第2版) 15.1 require 函数。

那么,要更新自己,只需要把 package.preload 和 package.loaded 清除,然后再用新的 模块填充 package.preload 即可。下面就是核心代码了:

1
2
3
4
5
6
7
8
9
10
11
12
local updatePackage = {
"update.UpdateApp",
"update.updater",
"update.updateScene",
}
for __,v in ipairs(updatePackage) do
package.preload[v] = nil
package.loaded[v] = nil
end
_G["update"] = nil
CCLuaLoadChunksFromZIP(newUpdatePackage)
require("update.UpdateApp").new("update"):run(false)

如果不相信这么简单,可以用上面完整的 UpdateApp 模块中提供的 UpdateApp:_printPackages(label) 方法来检测。

4.3.4 显示更新界面 update.UpdateApp:runUpdateScene(handler)

update.updater.checkUpdate() 的返回是异步的,下载和解压都需要时间,在这段时间里面,我们需要一个界面。runUpdateScene 方法的作用就是显示这个界面。并在更新成功之后调用handler处理函数。

4.3.5 显示主场景 update.UpdateApp:runRootScene()

到了这里,update 包就没有作用了。但由于我们先前没有载入除 update 包外的任何包,这里必须先载入它们。

我上面提到过,finalRes 这个全局变量是一个索引表,它的 lib 对象就是一个包含所有待载入的包(类似于 frameworks_precompiled.zip 这种)的列表。我们通过循环将它们载入内存。

对于 root.RootScene 这个模块来说,就是标准的quick模块了,它可以使用quick中的任何特性。

1
2
3
4
5
6
for __, v in pairs(finalRes.lib) do
print("runRootScene:CCLuaLoadChunksFromZip",__, v)
CCLuaLoadChunksFromZIP(v)
end

require("root.RootScene").new("root"):run()

4.3.6 怎么使用这个模块

你如果要直接拿来就用,这个模块基本上不需要修改。因为本来它就没什么特别的功能。当然,你可以看完下面两个模块再决定。

4.4 update.updateScene

这个模块用于显示更新过程的进度和一些信息。所有内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
------
-- updateScene for update package.
-- This is a object, not a class.
-- In this scene, it will show download progress bar
-- and state for uncompress.
-- @author zrong(zengrong.net)
-- Creation: 2014-07-03

local updater = require("update.updater")
local sharedDirector = CCDirector:sharedDirector()

-- check device screen size
local glview = sharedDirector:getOpenGLView()
local size = glview:getFrameSize()
local display = {}
display.sizeInPixels = {width = size.width, height = size.height}

local w = display.sizeInPixels.width
local h = display.sizeInPixels.height

CONFIG_SCREEN_WIDTH = 1280
CONFIG_SCREEN_HEIGHT = 800
CONFIG_SCREEN_AUTOSCALE = "FIXED_HEIGHT"

local scale, scaleX, scaleY

scaleX, scaleY = w / CONFIG_SCREEN_WIDTH, h / CONFIG_SCREEN_HEIGHT
scale = scaleY
CONFIG_SCREEN_WIDTH = w / scale

glview:setDesignResolutionSize(CONFIG_SCREEN_WIDTH, CONFIG_SCREEN_HEIGHT, kResolutionNoBorder)

local winSize = sharedDirector:getWinSize()
display.contentScaleFactor = scale
display.size = {width = winSize.width, height = winSize.height}
display.width = display.size.width
display.height = display.size.height
display.cx = display.width / 2
display.cy = display.height / 2
display.c_left = -display.width / 2
display.c_right = display.width / 2
display.c_top = display.height / 2
display.c_bottom = -display.height / 2
display.left = 0
display.right = display.width
display.top = display.height
display.bottom = 0
display.widthInPixels = display.sizeInPixels.width
display.heightInPixels = display.sizeInPixels.height

print("# display in updateScene start")
print(string.format("# us.CONFIG_SCREEN_AUTOSCALE = %s", CONFIG_SCREEN_AUTOSCALE))
print(string.format("# us.CONFIG_SCREEN_WIDTH = %0.2f", CONFIG_SCREEN_WIDTH))
print(string.format("# us.CONFIG_SCREEN_HEIGHT = %0.2f", CONFIG_SCREEN_HEIGHT))
print(string.format("# us.display.widthInPixels = %0.2f", display.widthInPixels))
print(string.format("# us.display.heightInPixels = %0.2f", display.heightInPixels))
print(string.format("# us.display.contentScaleFactor = %0.2f", display.contentScaleFactor))
print(string.format("# us.display.width = %0.2f", display.width))
print(string.format("# us.display.height = %0.2f", display.height))
print(string.format("# us.display.cx = %0.2f", display.cx))
print(string.format("# us.display.cy = %0.2f", display.cy))
print(string.format("# us.display.left = %0.2f", display.left))
print(string.format("# us.display.right = %0.2f", display.right))
print(string.format("# us.display.top = %0.2f", display.top))
print(string.format("# us.display.bottom = %0.2f", display.bottom))
print(string.format("# us.display.c_left = %0.2f", display.c_left))
print(string.format("# us.display.c_right = %0.2f", display.c_right))
print(string.format("# us.display.c_top = %0.2f", display.c_top))
print(string.format("# us.display.c_bottom = %0.2f", display.c_bottom))
print("# display in updateScene done")

display.ANCHOR_POINTS = {
CCPoint(0.5, 0.5), -- CENTER
CCPoint(0, 1), -- TOP_LEFT
CCPoint(0.5, 1), -- TOP_CENTER
CCPoint(1, 1), -- TOP_RIGHT
CCPoint(0, 0.5), -- CENTER_LEFT
CCPoint(1, 0.5), -- CENTER_RIGHT
CCPoint(0, 0), -- BOTTOM_LEFT
CCPoint(1, 0), -- BOTTOM_RIGHT
CCPoint(0.5, 0), -- BOTTOM_CENTER
}

display.CENTER = 1
display.LEFT_TOP = 2; display.TOP_LEFT = 2
display.CENTER_TOP = 3; display.TOP_CENTER = 3
display.RIGHT_TOP = 4; display.TOP_RIGHT = 4
display.CENTER_LEFT = 5; display.LEFT_CENTER = 5
display.CENTER_RIGHT = 6; display.RIGHT_CENTER = 6
display.BOTTOM_LEFT = 7; display.LEFT_BOTTOM = 7
display.BOTTOM_RIGHT = 8; display.RIGHT_BOTTOM = 8
display.BOTTOM_CENTER = 9; display.CENTER_BOTTOM = 9

function display.align(target, anchorPoint, x, y)
target:setAnchorPoint(display.ANCHOR_POINTS[anchorPoint])
if x and y then target:setPosition(x, y) end
end

local us = CCScene:create()
us.name = "updateScene"

local localResInfo = nil

function us._addUI()
-- Get the newest resinfo in ures.
local localResInfo = updater.getResCopy()

local __bg = CCSprite:create(us._getres("res/pic/init_bg.png"))
display.align(__bg, display.CENTER, display.cx, display.cy)
us:addChild(__bg, 0)

local __label = CCLabelTTF:create("Loading...", "Arial", 24)
__label:setColor(ccc3(255, 0, 0))
us._label = __label
display.align(__label, display.CENTER, display.cx, display.bottom+30)
us:addChild(__label, 10)
end

function us._getres(path)
if not localResInfo then
localResInfo = updater.getResCopy()
end
for key, value in pairs(localResInfo.oth) do
print("us._getres:", key, value)
local pathInIndex = string.find(key, path)
if pathInIndex and pathInIndex >= 1 then
print("us._getres getvalue:", path)
res[path] = value
return value
end
end
return path
end

function us._sceneHandler(event)
if event == "enter" then
print(string.format("updateScene \"%s:onEnter()\"", us.name))
us.onEnter()
elseif event == "cleanup" then
print(string.format("updateScene \"%s:onCleanup()\"", us.name))
us.onCleanup()
elseif event == "exit" then
print(string.format("updateScene \"%s:onExit()\"", us.name))
us.onExit()

if DEBUG_MEM then
print("----------------------------------------")
print(string.format("LUA VM MEMORY USED: %0.2f KB", collectgarbage("count")))
CCTextureCache:sharedTextureCache():dumpCachedTextureInfo()
print("----------------------------------------")
end
end
end

function us._updateHandler(event, value)
updater.state = event
if event == "success" then
updater.stateValue = value:getCString()
updater.updateFinalResInfo()
if us._succHandler then
us._succHandler()
end
elseif event == "error" then
updater.stateValue = value:getCString()
elseif event == "progress" then
updater.stateValue = tostring(value:getValue())
elseif event == "state" then
updater.stateValue = value:getCString()
end
us._label:setString(updater.stateValue)
assert(event ~= "error",
string.format("Update error: %s !", updater.stateValue))
end

function us.addListener(handler)
us._succHandler = handler
return us
end

function us.onEnter()
updater.update(us._updateHandler)
end

function us.onExit()
updater.clean()
us:unregisterScriptHandler()
end

function us.onCleanup()
end

us:registerScriptHandler(us._sceneHandler)
us._addUI()
return us

代码都在上面,说重点:

4.4.1 还是没有framework

这是必须一直牢记的。由于没有载入quick的 framework,所有的quick特性都不能使用。

你也许会说没有framework我怎么写界面?那么想想用C++的同学吧!那个代码怎么也比Lua多吧?

什么什么?你说有CCB和CCS?CCS你妹啊!同学我和你不是一个班的。

例如,原来在quick中这样写:

1
2
3
display.newSprite("res/pic/init_bg.png")
:align(display.CENTER, display.cx, display.cy)
:addTo(self, 0)

在没有quick framework的时候需要改成这样:

1
2
3
local __bg = CCSprite:create(us._getres("res/pic/init_bg.png"))
display.align(__bg, display.CENTER, display.cx, display.cy)
us:addChild(__bg, 0)

等等!为啥我用了 display !!!笨蛋,你不会偷quick的啊啊啊!

4.4.2 必须要偷的代码

为了方便使用,我们可以偷一部分framework的代码过来(干嘛说得那么难听嘛,程序员怎么能用偷?程序员的事,用CV啊),注意CV来的代码用local变量来保存。由于 updateScene 已经是一个可视的场景,因此quick中关于界面缩放设置的那部分代码也是必须CV过来。不多,几十行而已。

游戏产品绝大多数都不会做成横屏竖屏自动适应的(自己找SHI啊有木有),因此界面缩放的代码我也只保存了一个横屏的,这又省了不少。那CV的同学,注意自己改啊!

4.4.3 update.updateScene._getres(path)

在 update.updateScene 模块中,所有涉及到资源路径的地方,必须使用这个方法来包裹。

这个方法先从 update.updater 模块中获取最新的资源索引列表,然后根据我们传递的相对路径从索引列表中查找到资源的实际路径(可能是原包自带的资源,也可能是更新后的资源的绝对路径),然后载入它们。这能保证我们使用的是最新的资源。

4.4.4 update.updateScene._updateHandler(event, value)

这个方法已经在 上面 C++ 模块中 讲过了。注意其中的 _succHandler 是在 update.UpdateApp 中定义的匿名函数。

4.4.5 怎么使用这个模块

如果你要使用这个模块,那么可能大部分都要重写。你可以看到,我在这个模块中只有一个背景图和一个 CCLabeTTF 来显示下载进度和状态。你当然不希望你的更新界面就是这个样子。怎么也得来个妹子做封面不是?

4.5 update.updater

这是整个更新系统的核心部分了。代码更长一点,但其实很好懂。

在这个模块中,我们需要完成下面的工作:

  1. 调用C++的Updater模块来获取远程的版本号以及资源下载地址;
  2. 调用C++的Updater模块来下载解压;
  3. 合并解压后的新资源到新资源文件夹;
  4. 更新总的资源索引;
  5. 删除临时文件;
  6. 报告更新中的各种错误。

所以说,这是一个工具模块。它提供的是给更新使用的各种工具。而 UpdateApp 和 updateScene 则分别是功能和界面模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
--- The helper for update package.
-- It can download resources and uncompress it,
-- copy new package to res directory,
-- and remove temporery directory.
-- @author zrong(zengrong.net)
-- Creation 2014-07-03

require "lfs"
local updater = {}
updater.STATES = {
kDownStart = "downloadStart",
kDownDone = "downloadDone",
kUncompressStart = "uncompressStart",
kUncompressDone = "uncompressDone",
unknown = "stateUnknown",
}

updater.ERRORS = {
kCreateFile = "errorCreateFile",
kNetwork = "errorNetwork",
kNoNewVersion = "errorNoNewVersion",
kUncompress = "errorUncompress",
unknown = "errorUnknown";
}

function updater.isState(state)
for k,v in pairs(updater.STATES) do
if v == state then
return true
end
end
return false
end

function updater.clone(object)
local lookup_table = {}
local function _copy(object)
if type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table = {}
lookup_table[object] = new_table
for key, value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
return setmetatable(new_table, getmetatable(object))
end
return _copy(object)
end

function updater.vardump(object, label, returnTable)
local lookupTable = {}
local result = {}

local function _v(v)
if type(v) == "string" then
v = "\"" .. v .. "\""
end
return tostring(v)
end

local function _vardump(object, label, indent, nest)
label = label or ""
local postfix = ""
if nest > 1 then postfix = "," end
if type(object) ~= "table" then
if type(label) == "string" then
result[#result +1] = string.format("%s[\"%s\"] = %s%s", indent, label, _v(object), postfix)
else
result[#result +1] = string.format("%s%s%s", indent, _v(object), postfix)
end
elseif not lookupTable[object] then
lookupTable[object] = true

if type(label) == "string" then
result[#result +1 ] = string.format("%s%s = {", indent, label)
else
result[#result +1 ] = string.format("%s{", indent)
end
local indent2 = indent .. " "
local keys = {}
local values = {}
for k, v in pairs(object) do
keys[#keys + 1] = k
values[k] = v
end
table.sort(keys, function(a, b)
if type(a) == "number" and type(b) == "number" then
return a < b
else
return tostring(a) < tostring(b)
end
end)
for i, k in ipairs(keys) do
_vardump(values[k], k, indent2, nest + 1)
end
result[#result +1] = string.format("%s}%s", indent, postfix)
end
end
_vardump(object, label, "", 1)

if returnTable then return result end
return table.concat(result, "\n")
end

local u = nil
local f = CCFileUtils:sharedFileUtils()
-- The res index file in original package.
local lresinfo = "res/resinfo.lua"
local uroot = f:getWritablePath()
-- The directory for save updated files.
local ures = uroot.."res/"
-- The package zip file what download from server.
local uzip = uroot.."res.zip"
-- The directory for uncompress res.zip.
local utmp = uroot.."utmp/"
-- The res index file in zip package for update.
local zresinfo = utmp.."res/resinfo.lua"

-- The res index file for final game.
-- It combiled original lresinfo and zresinfo.
local uresinfo = ures .. "resinfo.lua"

local localResInfo = nil
local remoteResInfo = nil
local finalResInfo = nil

local function _initUpdater()
print("initUpdater, ", u)
if not u then u = Updater:new() end
print("after initUpdater:", u)
end

function updater.writeFile(path, content, mode)
mode = mode or "w+b"
local file = io.open(path, mode)
if file then
if file:write(content) == nil then return false end
io.close(file)
return true
else
return false
end
end

function updater.readFile(path)
return f:getFileData(path)
end

function updater.exists(path)
return f:isFileExist(path)
end

--[[
-- Departed, uses lfs instead.
function updater._mkdir(path)
_initUpdater()
return u:createDirectory(path)
end

-- Departed, get a warning in ios simulator
function updater._rmdir(path)
_initUpdater()
return u:removeDirectory(path)
end
--]]

function updater.mkdir(path)
if not updater.exists(path) then
return lfs.mkdir(path)
end
return true
end

function updater.rmdir(path)
print("updater.rmdir:", path)
if updater.exists(path) then
local function _rmdir(path)
local iter, dir_obj = lfs.dir(path)
while true do
local dir = iter(dir_obj)
if dir == nil then break end
if dir ~= "." and dir ~= ".." then
local curDir = path..dir
local mode = lfs.attributes(curDir, "mode")
if mode == "directory" then
_rmdir(curDir.."/")
elseif mode == "file" then
os.remove(curDir)
end
end
end
local succ, des = os.remove(path)
if des then print(des) end
return succ
end
_rmdir(path)
end
return true
end

-- Is there a update.zip package in ures directory?
-- If it is true, return its abstract path.
function updater.hasNewUpdatePackage()
local newUpdater = ures.."lib/update.zip"
if updater.exists(newUpdater) then
return newUpdater
end
return nil
end

-- Check local resinfo and remote resinfo, compare their version value.
function updater.checkUpdate()
localResInfo = updater.getLocalResInfo()
local localVer = localResInfo.version
print("localVer:", localVer)
remoteResInfo = updater.getRemoteResInfo(localResInfo.update_url)
local remoteVer = remoteResInfo.version
print("remoteVer:", remoteVer)
return remoteVer ~= localVer
end

-- Copy resinfo.lua from original package to update directory(ures)
-- when it is not in ures.
function updater.getLocalResInfo()
print(string.format("updater.getLocalResInfo, lresinfo:%s, uresinfo:%s",
lresinfo,uresinfo))
local resInfoTxt = nil
if updater.exists(uresinfo) then
resInfoTxt = updater.readFile(uresinfo)
else
assert(updater.mkdir(ures), ures.." create error!")
local info = updater.readFile(lresinfo)
print("localResInfo:", info)
assert(info, string.format("Can not get the constent from %s!", lresinfo))
updater.writeFile(uresinfo, info)
resInfoTxt = info
end
return assert(loadstring(resInfoTxt))()
end

function updater.getRemoteResInfo(path)
_initUpdater()
print("updater.getRemoteResInfo:", path)
local resInfoTxt = u:getUpdateInfo(path)
print("resInfoTxt:", resInfoTxt)
return assert(loadstring(resInfoTxt))()
end

function updater.update(handler)
assert(remoteResInfo and remoteResInfo.package, "Can not get remoteResInfo!")
print("updater.update:", remoteResInfo.package)
if handler then
u:registerScriptHandler(handler)
end
updater.rmdir(utmp)
u:update(remoteResInfo.package, uzip, utmp, false)
end

function updater._copyNewFile(resInZip)
-- Create nonexistent directory in update res.
local i,j = 1,1
while true do
j = string.find(resInZip, "/", i)
if j == nil then break end
local dir = string.sub(resInZip, 1,j)
-- Save created directory flag to a table because
-- the io operation is too slow.
if not updater._dirList[dir] then
updater._dirList[dir] = true
local fullUDir = uroot..dir
updater.mkdir(fullUDir)
end
i = j+1
end
local fullFileInURes = uroot..resInZip
local fullFileInUTmp = utmp..resInZip
print(string.format('copy %s to %s', fullFileInUTmp, fullFileInURes))
local zipFileContent = updater.readFile(fullFileInUTmp)
if zipFileContent then
updater.writeFile(fullFileInURes, zipFileContent)
return fullFileInURes
end
return nil
end

function updater._copyNewFilesBatch(resType, resInfoInZip)
local resList = resInfoInZip[resType]
if not resList then return end
local finalRes = finalResInfo[resType]
for __,v in ipairs(resList) do
local fullFileInURes = updater._copyNewFile(v)
if fullFileInURes then
-- Update key and file in the finalResInfo
-- Ignores the update package because it has been in memory.
if v ~= "res/lib/update.zip" then
finalRes[v] = fullFileInURes
end
else
print(string.format("updater ERROR, copy file %s.", v))
end
end
end

function updater.updateFinalResInfo()
assert(localResInfo and remoteResInfo,
"Perform updater.checkUpdate() first!")
if not finalResInfo then
finalResInfo = updater.clone(localResInfo)
end
--do return end
local resInfoTxt = updater.readFile(zresinfo)
local zipResInfo = assert(loadstring(resInfoTxt))()
if zipResInfo["version"] then
finalResInfo.version = zipResInfo["version"]
end
-- Save a dir list maked.
updater._dirList = {}
updater._copyNewFilesBatch("lib", zipResInfo)
updater._copyNewFilesBatch("oth", zipResInfo)
-- Clean dir list.
updater._dirList = nil
updater.rmdir(utmp)
local dumpTable = updater.vardump(finalResInfo, "local data", true)
dumpTable[#dumpTable+1] = "return data"
if updater.writeFile(uresinfo, table.concat(dumpTable, "\n")) then
return true
end
print(string.format("updater ERROR, write file %s.", uresinfo))
return false
end

function updater.getResCopy()
if finalResInfo then return updater.clone(finalResInfo) end
return updater.clone(localResInfo)
end

function updater.clean()
if u then
u:unregisterScriptHandler()
u:delete()
u = nil
end
updater.rmdir(utmp)
localResInfo = nil
remoteResInfo = nil
finalResInfo = nil
end

return updater

代码都在上面,还是说重点:

4.5.1 就是没有framework

我嘴巴都说出茧子了,没有就是没有。

不过,我又从quick CV了几个方法过来:

  • clone 方法用来完全复制一个table,在复制文件索引列表的时候使用;
  • vardump 方法用来1持久化索引列表,使其作为一个lua文件保存在设备存储器上。有修改。
  • writeFile 和 readFile 用于把需要的文件写入设备中,也用它来复制文件(读入一个文件,在另一个地方写入来实现复制)
  • exists 这个和quick实现的不太一样,直接用 CCFileUtils 了。

4.5.2 文件操作

除了可以用 writeFile 和 readFile 来实现文件的复制操作之外,还要实现文件夹的创建和删除。

这个功能可以使用 lfs(Lua file system) 来实现,参见:在lua中递归删除一个文件夹

4.5.3 相关目录和变量

上面的代码中定义了几个变量,在这里进行介绍方便理解:

4.5.3.1 lres(local res)

安装包所带的res目录;

4.5.3.2 ures(updated res)

保存在设备上的res目录,用于保存从网上下载的新资源;

4.5.3.3 utmp(update temp)

临时文件夹,用于解压缩,更新后会删除;

4.5.3.4 lresinfo(本地索引文件)

安装包内自带的所有资源的索引文件,所有资源路径指向包内自带的资源。打包的时候和产品包一起提供,产品包会默认使用这个资源索引文件来查找资源。它的大概内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local data = {
version = "1.0",
update_url = "http://192.168.18.22:8080/updater/resinfo.lua",
lib = {
["res/lib/config.zip"] = "res/lib/config.zip",
["res/lib/framework_precompiled.zip"] = "res/lib/framework_precompiled.zip",
["res/lib/root.zip"] = "res/lib/root.zip",
......
},
oth = {
["res/pic/init_bg.png"] = "res/pic/init_bg.png",
......
},
}
return data

从它的结构可以看出,它包含了当前包的版本(version)、在哪里获取要更新的资源索引文件(update_url)、当前包中所有的lua模块的路径(lib)、当前包中所有的资源文件的路径(oth)。

4.5.3.5 uresinfo(更新索引文件)

保存在 ures 中的更新后的索引文件,没有更新的资源路径指向包内自带的资源,更新后的资源路径指向ures中的资源。它的内容大致如下:

config.zip 的路径是在 iOS 模拟器中得到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local data = {
version = "1.0",
update_url = "http://192.168.18.22:8080/updater/resinfo.lua",
lib = {
["res/lib/cc.zip"] = "res/lib/cc.zip",
["res/lib/config.zip"] = "/Users/zrong/Library/Application Support/iPhone Simulator/7.1/Applications/2B46FAC0-C419-42B5-92B0-B06DD16E113B/Documents/res/lib/config.zip",
......
},
oth = {
["res/pic/init_bg.png"] = "res/pic/init_bg.png",
......
},
}
return data

4.5.3.6 http://192.168.18.22:8080/updater/resinfo.lua

getRemoteResInfo 方法会读取这个文件,然后将结果解析成lua table。对比其中的version与 lrefinfo 中的区别,来决定是否需要更新。

若需要,则调用C++ Updater模块中的方法下载 package 指定的zip包并解压。

它的内容如下:

1
2
3
4
5
local data = {
version = "1.0.2",
package = "http://192.168.18.22:8080/updater/res.zip",
}
return data

4.5.3.7 http://192.168.18.22:8080/updater/res.zip

zip包的文件夹结构大致如下:

res/
res/resinfo.lua
res/lib/cc.zip
res/pic/init_bg.png
......

zip文件的下载和解压都是由C++完成的,但是下载和解压的路径需要Lua来提供。这个动作完成后,C++会通知Lua更新成功。Lua会接着进行后续操作就使用下面 4.5.4 中提到的方法来复制资源、合并 uresinfo 。

4.5.3.8 zresinfo(zip资源索引文件)

zip文件中也包含一个 resinfo.lua ,它用于指示哪些文件需要更新。内容大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
local data = {
version = "1.0.2",
lib = {
"res/lib/cc.zip",
......
},
oth = {
"res/pic/init_bg",
......
},
}
return data

这个文件中包含的所有文件必须能在zip解压后找到。


4.5.4 update.updater.updateFinalResInfo()

这是一个至关重要的方法,让我们代入用上面提到的变量名和目录来描述它的功能:

它实现的功能是:

  1. 读取 uresinfo,若没有,则将 lresinfo 复制成 uresinfo;
  2. 从 utmp 中读取 zresinfo,注意此时zip文件已经解压;
  3. 将需要更新的资源文件从 utmp 中复制到 ures 中;
  4. 更新 uresinfo ,使其中的资源键名指向正确的资源路径(上一步复制的目标路径);
  5. 删除 utmp;
  6. 将更新后的 uresinfo 作为lua文件写入 ures 。

4.5.5 其它方法

对 update.updater 的调用一般是这样的顺序:

  1. 调用 checkUpdat 方法检测是否需要升级;
  2. 调用 update 方法执行升级,同时注册事件管理handler;
  3. 升级成功,调用 getResCopy 方法获取最新的 uresinfo 。

5 对 framework 的修改

5.1 写一个 getres 方法

ures 中包含的就是所有素材的索引(键值对)。形式如下:

  • 键名:res/pic/init_bg.png
  • 键值(lres中): res/pic/init_bg.png
  • 键值(ures中):/Users/zrong/Library/Application Support/iPhone Simulator/7.1/Applications/2B46FAC0-C419-42B5-92B0-B06DD16E113B/Documents/res/pic/init_bg.png

在程序中,我们一般会使用这样的写法来获取资源:

1
display.newSprite("pic/init_bg.png")

或者干脆简化成了:

1
display.newSprite("init_bg.png")

要上面的代码能够工作,需要为 CCFileUtils 设置搜索路径:

1
2
CCFileUtils:sharedFileUtils:addSearchPath("res/")
CCFileUtils:sharedFileUtils:addSearchPath("res/pic/")

但是,在这套更新机制中,我不建议设置搜索路径,因为素材都是以完整路径格式保存的,这样使用起来更方便和更确定。

如果是新项目,那么挺好,我只需要保证素材路径基于 res 提供即可,类似这样:

1
display.newSprite("res/pic/init_bg.png")

但是对于已经开发了一段时间的项目来说,一个个改就太不专业了。这是我们需要扩展一个 io.getres 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
res = {}

function io.getres(path)
print("io.getres originl:", path)
if CCFileUtils:sharedFileUtils():isAbsolutePath(path) then
return path
end
if res[path] then return res[path] end
for key, value in pairs(finalRes.oth) do
print(key, value)
local pathInIndex = string.find(key, path)
if pathInIndex and pathInIndex >= 1 then
print("io.getres getvalue:", path)
res[path] = value
return value
end
end
print("io.getres no get:", path)
return path
end

然后,我们需要修改 quick framework 中的display模块让我们的旧代码不必进行任何改动就能生效。

5.2 修改 display.newSprite

找到该方法中的这个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if string.byte(filename) == 35 then -- first char is #
local frame = display.newSpriteFrame(string.sub(filename, 2))
if frame then
sprite = spriteClass:createWithSpriteFrame(frame)
end
else
if display.TEXTURES_PIXEL_FORMAT[filename] then
CCTexture2D:setDefaultAlphaPixelFormat(display.TEXTURES_PIXEL_FORMAT[filename])
sprite = spriteClass:create(filename)
CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA8888)
else
sprite = spriteClass:create(filename)
end
end

将其改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if string.byte(filename) == 35 then -- first char is #
local frame = display.newSpriteFrame(string.sub(filename, 2))
if frame then
sprite = spriteClass:createWithSpriteFrame(frame)
end
else
local absfilename = io.getres(filename)
if display.TEXTURES_PIXEL_FORMAT[filename] then
CCTexture2D:setDefaultAlphaPixelFormat(display.TEXTURES_PIXEL_FORMAT[filename])
sprite = spriteClass:create(absfilename)
CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA8888)
else
sprite = spriteClass:create(absfilename)
end
end

5.3 修改display.newTilesSprite

将其中的 local sprite = CCSprite:create(filename, rect)

改为local sprite = CCSprite:create(io.getres(filename), rect)

5.4 修改 display.newBatchNode

改法与上面相同。

6. 后记

噢!这真是一篇太长的文章了,真希望我都说清了。

其实还有一些东西在这个机制中没有涉及,例如:

6.1 更新的健壮性

  • 在更新 update.zip 模块自身的时候,如果新的update.zip有问题怎么办?
  • 如果索引文件找不到怎么办?zip文件解压失败怎么办?zresinfo 中的内容与zip文件解压后的内容不符怎么办?
  • 下载更新的时候网断了如何处理?如何处理断点续传?设备磁盘空间不够了怎么处理?

6.2 更多的更新方式

我在 需求的复杂性 里面描述了一些需求,例如:

  • 如何回滚更新?
  • 如何多个版本共存?
  • 如何对资源进行指纹码化?

这些问题都不难解决。方法自己想,我只能写到这儿了。

话说回来,实现了 更新一切 ,你还担心什么呢?



射手,30分钟够么?

留言