quick-cocos2d-x的热更新机制实现
0 依赖
这里说的热更新,指的是客户端的更新。
大致的流程是,客户端在启动后访问更新api,根据更新api的反馈,下载更新资源,然后使用新的资源启动客户端,或者直接使用新资源不重启客户端。
这种方式可以跳过AppStore的审核,避免了用户频繁下载、安装、覆盖产品包。
我们一般使用这种方式快速修复产品BUG和增加新功能。
本文基于 quick-cocos2d-x zrong 修改版 。
1 前言
1.1 他山之石
在实现这个机制之前,我研究了这几篇文章:
- quick-cocos2d-x基于源码加密打包功能的更新策略 by SunLightJuly
- 看到有同学在研究在线更新,希望我能帮到你一些 by Henry
- 基于Quick-cocos2dx 2.2.3 的动态更新实现完整篇 by 西门大官人
另外,我也查看了 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
之前进行载入。如下:
1bool AppDelegate::applicationDidFinishLaunching()
2{
3 // initialize director
4 CCDirector *pDirector = CCDirector::sharedDirector();
5 pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
6 pDirector->setProjection(kCCDirectorProjection2D);
7
8 // set FPS. the default value is 1.0/60 if you don't call this
9 pDirector->setAnimationInterval(1.0 / 60);
10
11 // register lua engine
12 CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
13 CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
14
15 CCLuaStack *pStack = pEngine->getLuaStack();
16
17#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
18 // load framework
19 pStack->loadChunksFromZIP("res/framework_precompiled.zip");
20
21 // set script path
22 string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("scripts/main.lua");
23 ......
这可以说明这个核心模块对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协议下载升级包,需要提供四个参数:
- zip文件的url;
- zip文件的保存位置;
- zip 文件的解压临时目录;
- 解压之前是否需要清空临时目录。
3.4 修改事件类型
我把把传递给lua的事件分成了四种类型:
3.4.1 UPDATER_MESSAGE_UPDATE_SUCCEED
事件名为 success,代表更新成功,zip文件下载并解压完毕;
3.4.2 UPDATER_MESSAGE_STATE
事件名为 state,更新过程中的状态(下载开始、结束,解压开始、结束)也传递给了lua。这个方法是这样实现的:
1void Updater::Helper::handlerState(Message *msg)
2{
3 StateMessage* stateMsg = (StateMessage*)msg->obj;
4 if(stateMsg->manager->_delegate)
5 {
6 stateMsg->manager->_delegate->onState(stateMsg->code);
7 }
8 if (stateMsg->manager->_scriptHandler)
9 {
10 std::string stateMessage;
11 switch ((StateCode)stateMsg->code)
12 {
13 case kDownStart:
14 stateMessage = "downloadStart";
15 break;
16
17 case kDownDone:
18 stateMessage = "downloadDone";
19 break;
20
21 case kUncompressStart:
22 stateMessage = "uncompressStart";
23 break;
24 case kUncompressDone:
25 stateMessage = "uncompressDone";
26 break;
27
28 default:
29 stateMessage = "stateUnknown";
30 }
31
32 CCScriptEngineManager::sharedManager()
33 ->getScriptEngine()
34 ->executeEvent(
35 stateMsg->manager->_scriptHandler,
36 "state",
37 CCString::create(stateMessage.c_str()),
38 "CCString");
39 }
40
41 delete ((StateMessage*)msg->obj);
42}
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函数的例子。
1function us._updateHandler(event, value)
2 updater.state = event
3 if event == "success" then
4 updater.stateValue = value:getCString()
5 -- 成功之后更新资源列表,合并新资源
6 updater.updateFinalResInfo()
7 -- 调用成功后的处理函数
8 if us._succHandler then
9 us._succHandler()
10 end
11 elseif event == "error" then
12 updater.stateValue = value:getCString()
13 elseif event == "progress" then
14 updater.stateValue = tostring(value:getValue())
15 elseif event == "state" then
16 updater.stateValue = value:getCString()
17 end
18 -- us._label 是一个CCLabelTTF,用来显示进度和状态
19 us._label:setString(updater.stateValue)
20 assert(event ~= "error",
21 string.format("Update error: %s !", updater.stateValue))
22end
23
24updater:registerScriptHandler(us._updateHandler)
4. update包(lua)
update包是整个项目的入口包,quick会首先载入这个包,甚至在 framework 之前。
4.1 为update包所做的项目修改
我修改了quick项目文件 AppDelegate.cpp 中的 applicationDidFinishLaunching
方法,使其变成这样:
1bool AppDelegate::applicationDidFinishLaunching()
2{
3 // initialize director
4 CCDirector *pDirector = CCDirector::sharedDirector();
5 pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
6 pDirector->setProjection(kCCDirectorProjection2D);
7
8 // set FPS. the default value is 1.0/60 if you don't call this
9 pDirector->setAnimationInterval(1.0 / 60);
10
11 // register lua engine
12 CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
13 CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
14
15 CCLuaStack *pStack = pEngine->getLuaStack();
16
17 string gtrackback = "\
18 function __G__TRACKBACK__(errorMessage) \
19 print(\"----------------------------------------\") \
20 print(\"LUA ERROR: \" .. tostring(errorMessage) .. \"\\n\") \
21 print(debug.traceback(\"\", 2)) \
22 print(\"----------------------------------------\") \
23 end";
24 pEngine->executeString(gtrackback.c_str());
25
26 // load update framework
27 pStack->loadChunksFromZIP("res/lib/update.zip");
28
29 string start_path = "require(\"update.UpdateApp\").new(\"update\"):run(true)";
30 CCLOG("------------------------------------------------");
31 CCLOG("EXECUTE LUA STRING: %s", start_path.c_str());
32 CCLOG("------------------------------------------------");
33 pEngine->executeString(start_path.c_str());
34
35 return true;
36}
原来位于 main.lua 中的 __G_TRACKBACK__
函数(用于输出lua报错信息)直接包含在C++代码中了。那么现在 main.lua
就不再需要了。
同样的,第一个载入的模块变成了 res/lib/update.zip
,这个zip也可以放在quick能找到的其它路径中,使用这个路径只是我的个人习惯。
最后,LuaStack直接执行了下面这句代码启动了 update.UpdateApp
模块:
1require("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-- @author zrong(zengrong.net)
2-- Creation 2014-07-03
3
4local UpdateApp = {}
5
6UpdateApp.__cname = "UpdateApp"
7UpdateApp.__index = UpdateApp
8UpdateApp.__ctype = 2
9
10local sharedDirector = CCDirector:sharedDirector()
11local sharedFileUtils = CCFileUtils:sharedFileUtils()
12local updater = require("update.updater")
13
14function UpdateApp.new(...)
15 local instance = setmetatable({}, UpdateApp)
16 instance.class = UpdateApp
17 instance:ctor(...)
18 return instance
19end
20
21function UpdateApp:ctor(appName, packageRoot)
22 self.name = appName
23 self.packageRoot = packageRoot or appName
24
25 print(string.format("UpdateApp.ctor, appName:%s, packageRoot:%s", appName, packageRoot))
26
27 -- set global app
28 _G[self.name] = self
29end
30
31function UpdateApp:run(checkNewUpdatePackage)
32 --print("I am new update package")
33 local newUpdatePackage = updater.hasNewUpdatePackage()
34 print(string.format("UpdateApp.run(%s), newUpdatePackage:%s",
35 checkNewUpdatePackage, newUpdatePackage))
36 if checkNewUpdatePackage and newUpdatePackage then
37 self:updateSelf(newUpdatePackage)
38 elseif updater.checkUpdate() then
39 self:runUpdateScene(function()
40 _G["finalRes"] = updater.getResCopy()
41 self:runRootScene()
42 end)
43 else
44 _G["finalRes"] = updater.getResCopy()
45 self:runRootScene()
46 end
47end
48
49-- Remove update package, load new update package and run it.
50function UpdateApp:updateSelf(newUpdatePackage)
51 print("UpdateApp.updateSelf ", newUpdatePackage)
52 local updatePackage = {
53 "update.UpdateApp",
54 "update.updater",
55 "update.updateScene",
56 }
57 self:_printPackages("--before clean")
58 for __,v in ipairs(updatePackage) do
59 package.preload[v] = nil
60 package.loaded[v] = nil
61 end
62 self:_printPackages("--after clean")
63 _G["update"] = nil
64 CCLuaLoadChunksFromZIP(newUpdatePackage)
65 self:_printPackages("--after CCLuaLoadChunksForZIP")
66 require("update.UpdateApp").new("update"):run(false)
67 self:_printPackages("--after require and run")
68end
69
70-- Show a scene for update.
71function UpdateApp:runUpdateScene(handler)
72 self:enterScene(require("update.updateScene").addListener(handler))
73end
74
75-- Load all of packages(except update package, it is not in finalRes.lib)
76-- and run root app.
77function UpdateApp:runRootScene()
78 for __, v in pairs(finalRes.lib) do
79 print("runRootScene:CCLuaLoadChunksFromZip",__, v)
80 CCLuaLoadChunksFromZIP(v)
81 end
82
83 require("root.RootScene").new("root"):run()
84end
85
86function UpdateApp:_printPackages(label)
87 label = label or ""
88 print("\npring packages "..label.."------------------")
89 for __k, __v in pairs(package.preload) do
90 print("package.preload:", __k, __v)
91 end
92 for __k, __v in pairs(package.loaded) do
93 print("package.loaded:", __k, __v)
94 end
95 print("print packages "..label.."------------------\n")
96end
97
98
99function UpdateApp:exit()
100 sharedDirector:endToLua()
101 os.exit()
102end
103
104function UpdateApp:enterScene(__scene)
105 if sharedDirector:getRunningScene() then
106 sharedDirector:replaceScene(__scene)
107 else
108 sharedDirector:runWithScene(__scene)
109 end
110end
111
112return 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 即可。下面就是核心代码了:
1local updatePackage = {
2 "update.UpdateApp",
3 "update.updater",
4 "update.updateScene",
5}
6for __,v in ipairs(updatePackage) do
7 package.preload[v] = nil
8 package.loaded[v] = nil
9end
10_G["update"] = nil
11CCLuaLoadChunksFromZIP(newUpdatePackage)
12require("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中的任何特性。
1for __, v in pairs(finalRes.lib) do
2 print("runRootScene:CCLuaLoadChunksFromZip",__, v)
3 CCLuaLoadChunksFromZIP(v)
4end
5
6require("root.RootScene").new("root"):run()
4.3.6 怎么使用这个模块
你如果要直接拿来就用,这个模块基本上不需要修改。因为本来它就没什么特别的功能。当然,你可以看完下面两个模块再决定。
4.4 update.updateScene
这个模块用于显示更新过程的进度和一些信息。所有内容如下:
1-- updateScene for update package.
2-- This is a object, not a class.
3-- In this scene, it will show download progress bar
4-- and state for uncompress.
5-- @author zrong(zengrong.net)
6-- Creation: 2014-07-03
7
8local updater = require("update.updater")
9local sharedDirector = CCDirector:sharedDirector()
10
11-- check device screen size
12local glview = sharedDirector:getOpenGLView()
13local size = glview:getFrameSize()
14local display = {}
15display.sizeInPixels = {width = size.width, height = size.height}
16
17local w = display.sizeInPixels.width
18local h = display.sizeInPixels.height
19
20CONFIG_SCREEN_WIDTH = 1280
21CONFIG_SCREEN_HEIGHT = 800
22CONFIG_SCREEN_AUTOSCALE = "FIXED_HEIGHT"
23
24local scale, scaleX, scaleY
25
26scaleX, scaleY = w / CONFIG_SCREEN_WIDTH, h / CONFIG_SCREEN_HEIGHT
27scale = scaleY
28CONFIG_SCREEN_WIDTH = w / scale
29
30glview:setDesignResolutionSize(CONFIG_SCREEN_WIDTH, CONFIG_SCREEN_HEIGHT, kResolutionNoBorder)
31
32local winSize = sharedDirector:getWinSize()
33display.contentScaleFactor = scale
34display.size = {width = winSize.width, height = winSize.height}
35display.width = display.size.width
36display.height = display.size.height
37display.cx = display.width / 2
38display.cy = display.height / 2
39display.c_left = -display.width / 2
40display.c_right = display.width / 2
41display.c_top = display.height / 2
42display.c_bottom = -display.height / 2
43display.left = 0
44display.right = display.width
45display.top = display.height
46display.bottom = 0
47display.widthInPixels = display.sizeInPixels.width
48display.heightInPixels = display.sizeInPixels.height
49
50print("# display in updateScene start")
51print(string.format("# us.CONFIG_SCREEN_AUTOSCALE = %s", CONFIG_SCREEN_AUTOSCALE))
52print(string.format("# us.CONFIG_SCREEN_WIDTH = %0.2f", CONFIG_SCREEN_WIDTH))
53print(string.format("# us.CONFIG_SCREEN_HEIGHT = %0.2f", CONFIG_SCREEN_HEIGHT))
54print(string.format("# us.display.widthInPixels = %0.2f", display.widthInPixels))
55print(string.format("# us.display.heightInPixels = %0.2f", display.heightInPixels))
56print(string.format("# us.display.contentScaleFactor = %0.2f", display.contentScaleFactor))
57print(string.format("# us.display.width = %0.2f", display.width))
58print(string.format("# us.display.height = %0.2f", display.height))
59print(string.format("# us.display.cx = %0.2f", display.cx))
60print(string.format("# us.display.cy = %0.2f", display.cy))
61print(string.format("# us.display.left = %0.2f", display.left))
62print(string.format("# us.display.right = %0.2f", display.right))
63print(string.format("# us.display.top = %0.2f", display.top))
64print(string.format("# us.display.bottom = %0.2f", display.bottom))
65print(string.format("# us.display.c_left = %0.2f", display.c_left))
66print(string.format("# us.display.c_right = %0.2f", display.c_right))
67print(string.format("# us.display.c_top = %0.2f", display.c_top))
68print(string.format("# us.display.c_bottom = %0.2f", display.c_bottom))
69print("# display in updateScene done")
70
71display.ANCHOR_POINTS = {
72 CCPoint(0.5, 0.5), -- CENTER
73 CCPoint(0, 1), -- TOP_LEFT
74 CCPoint(0.5, 1), -- TOP_CENTER
75 CCPoint(1, 1), -- TOP_RIGHT
76 CCPoint(0, 0.5), -- CENTER_LEFT
77 CCPoint(1, 0.5), -- CENTER_RIGHT
78 CCPoint(0, 0), -- BOTTOM_LEFT
79 CCPoint(1, 0), -- BOTTOM_RIGHT
80 CCPoint(0.5, 0), -- BOTTOM_CENTER
81}
82
83display.CENTER = 1
84display.LEFT_TOP = 2; display.TOP_LEFT = 2
85display.CENTER_TOP = 3; display.TOP_CENTER = 3
86display.RIGHT_TOP = 4; display.TOP_RIGHT = 4
87display.CENTER_LEFT = 5; display.LEFT_CENTER = 5
88display.CENTER_RIGHT = 6; display.RIGHT_CENTER = 6
89display.BOTTOM_LEFT = 7; display.LEFT_BOTTOM = 7
90display.BOTTOM_RIGHT = 8; display.RIGHT_BOTTOM = 8
91display.BOTTOM_CENTER = 9; display.CENTER_BOTTOM = 9
92
93function display.align(target, anchorPoint, x, y)
94 target:setAnchorPoint(display.ANCHOR_POINTS[anchorPoint])
95 if x and y then target:setPosition(x, y) end
96end
97
98local us = CCScene:create()
99us.name = "updateScene"
100
101local localResInfo = nil
102
103function us._addUI()
104 -- Get the newest resinfo in ures.
105 local localResInfo = updater.getResCopy()
106
107 local __bg = CCSprite:create(us._getres("res/pic/init_bg.png"))
108 display.align(__bg, display.CENTER, display.cx, display.cy)
109 us:addChild(__bg, 0)
110
111 local __label = CCLabelTTF:create("Loading...", "Arial", 24)
112 __label:setColor(ccc3(255, 0, 0))
113 us._label = __label
114 display.align(__label, display.CENTER, display.cx, display.bottom+30)
115 us:addChild(__label, 10)
116end
117
118function us._getres(path)
119 if not localResInfo then
120 localResInfo = updater.getResCopy()
121 end
122 for key, value in pairs(localResInfo.oth) do
123 print("us._getres:", key, value)
124 local pathInIndex = string.find(key, path)
125 if pathInIndex and pathInIndex >= 1 then
126 print("us._getres getvalue:", path)
127 res[path] = value
128 return value
129 end
130 end
131 return path
132end
133
134function us._sceneHandler(event)
135 if event == "enter" then
136 print(string.format("updateScene \"%s:onEnter()\"", us.name))
137 us.onEnter()
138 elseif event == "cleanup" then
139 print(string.format("updateScene \"%s:onCleanup()\"", us.name))
140 us.onCleanup()
141 elseif event == "exit" then
142 print(string.format("updateScene \"%s:onExit()\"", us.name))
143 us.onExit()
144
145 if DEBUG_MEM then
146 print("----------------------------------------")
147 print(string.format("LUA VM MEMORY USED: %0.2f KB", collectgarbage("count")))
148 CCTextureCache:sharedTextureCache():dumpCachedTextureInfo()
149 print("----------------------------------------")
150 end
151 end
152end
153
154function us._updateHandler(event, value)
155 updater.state = event
156 if event == "success" then
157 updater.stateValue = value:getCString()
158 updater.updateFinalResInfo()
159 if us._succHandler then
160 us._succHandler()
161 end
162 elseif event == "error" then
163 updater.stateValue = value:getCString()
164 elseif event == "progress" then
165 updater.stateValue = tostring(value:getValue())
166 elseif event == "state" then
167 updater.stateValue = value:getCString()
168 end
169 us._label:setString(updater.stateValue)
170 assert(event ~= "error",
171 string.format("Update error: %s !", updater.stateValue))
172end
173
174function us.addListener(handler)
175 us._succHandler = handler
176 return us
177end
178
179function us.onEnter()
180 updater.update(us._updateHandler)
181end
182
183function us.onExit()
184 updater.clean()
185 us:unregisterScriptHandler()
186end
187
188function us.onCleanup()
189end
190
191us:registerScriptHandler(us._sceneHandler)
192us._addUI()
193return us
代码都在上面,说重点:
4.4.1 还是没有framework
这是必须一直牢记的。由于没有载入quick的 framework,所有的quick特性都不能使用。
你也许会说没有framework我怎么写界面?那么想想用C++的同学吧!那个代码怎么也比Lua多吧?
什么什么?你说有CCB和CCS?CCS你妹啊!同学我和你不是一个班的。
例如,原来在quick中这样写:
1display.newSprite("res/pic/init_bg.png")
2 :align(display.CENTER, display.cx, display.cy)
3 :addTo(self, 0)
在没有quick framework的时候需要改成这样:
1local __bg = CCSprite:create(us._getres("res/pic/init_bg.png"))
2display.align(__bg, display.CENTER, display.cx, display.cy)
3us: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
这是整个更新系统的核心部分了。代码更长一点,但其实很好懂。
在这个模块中,我们需要完成下面的工作:
- 调用C++的Updater模块来获取远程的版本号以及资源下载地址;
- 调用C++的Updater模块来下载解压;
- 合并解压后的新资源到新资源文件夹;
- 更新总的资源索引;
- 删除临时文件;
- 报告更新中的各种错误。
所以说,这是一个工具模块。它提供的是给更新使用的各种工具。而 UpdateApp 和 updateScene 则分别是功能和界面模块。
1-- It can download resources and uncompress it,
2-- copy new package to res directory,
3-- and remove temporery directory.
4-- @author zrong(zengrong.net)
5-- Creation 2014-07-03
6
7require "lfs"
8local updater = {}
9updater.STATES = {
10 kDownStart = "downloadStart",
11 kDownDone = "downloadDone",
12 kUncompressStart = "uncompressStart",
13 kUncompressDone = "uncompressDone",
14 unknown = "stateUnknown",
15}
16
17updater.ERRORS = {
18 kCreateFile = "errorCreateFile",
19 kNetwork = "errorNetwork",
20 kNoNewVersion = "errorNoNewVersion",
21 kUncompress = "errorUncompress",
22 unknown = "errorUnknown";
23}
24
25function updater.isState(state)
26 for k,v in pairs(updater.STATES) do
27 if v == state then
28 return true
29 end
30 end
31 return false
32end
33
34function updater.clone(object)
35 local lookup_table = {}
36 local function _copy(object)
37 if type(object) ~= "table" then
38 return object
39 elseif lookup_table[object] then
40 return lookup_table[object]
41 end
42 local new_table = {}
43 lookup_table[object] = new_table
44 for key, value in pairs(object) do
45 new_table[_copy(key)] = _copy(value)
46 end
47 return setmetatable(new_table, getmetatable(object))
48 end
49 return _copy(object)
50end
51
52function updater.vardump(object, label, returnTable)
53 local lookupTable = {}
54 local result = {}
55
56 local function _v(v)
57 if type(v) == "string" then
58 v = "\"" .. v .. "\""
59 end
60 return tostring(v)
61 end
62
63 local function _vardump(object, label, indent, nest)
64 label = label or ""
65 local postfix = ""
66 if nest > 1 then postfix = "," end
67 if type(object) ~= "table" then
68 if type(label) == "string" then
69 result[#result +1] = string.format("%s[\"%s\"] = %s%s", indent, label, _v(object), postfix)
70 else
71 result[#result +1] = string.format("%s%s%s", indent, _v(object), postfix)
72 end
73 elseif not lookupTable[object] then
74 lookupTable[object] = true
75
76 if type(label) == "string" then
77 result[#result +1 ] = string.format("%s%s = {", indent, label)
78 else
79 result[#result +1 ] = string.format("%s{", indent)
80 end
81 local indent2 = indent .. " "
82 local keys = {}
83 local values = {}
84 for k, v in pairs(object) do
85 keys[#keys + 1] = k
86 values[k] = v
87 end
88 table.sort(keys, function(a, b)
89 if type(a) == "number" and type(b) == "number" then
90 return a < b
91 else
92 return tostring(a) < tostring(b)
93 end
94 end)
95 for i, k in ipairs(keys) do
96 _vardump(values[k], k, indent2, nest + 1)
97 end
98 result[#result +1] = string.format("%s}%s", indent, postfix)
99 end
100 end
101 _vardump(object, label, "", 1)
102
103 if returnTable then return result end
104 return table.concat(result, "\n")
105end
106
107local u = nil
108local f = CCFileUtils:sharedFileUtils()
109-- The res index file in original package.
110local lresinfo = "res/resinfo.lua"
111local uroot = f:getWritablePath()
112-- The directory for save updated files.
113local ures = uroot.."res/"
114-- The package zip file what download from server.
115local uzip = uroot.."res.zip"
116-- The directory for uncompress res.zip.
117local utmp = uroot.."utmp/"
118-- The res index file in zip package for update.
119local zresinfo = utmp.."res/resinfo.lua"
120
121-- The res index file for final game.
122-- It combiled original lresinfo and zresinfo.
123local uresinfo = ures .. "resinfo.lua"
124
125local localResInfo = nil
126local remoteResInfo = nil
127local finalResInfo = nil
128
129local function _initUpdater()
130 print("initUpdater, ", u)
131 if not u then u = Updater:new() end
132 print("after initUpdater:", u)
133end
134
135function updater.writeFile(path, content, mode)
136 mode = mode or "w+b"
137 local file = io.open(path, mode)
138 if file then
139 if file:write(content) == nil then return false end
140 io.close(file)
141 return true
142 else
143 return false
144 end
145end
146
147function updater.readFile(path)
148 return f:getFileData(path)
149end
150
151function updater.exists(path)
152 return f:isFileExist(path)
153end
154
155--[[
156-- Departed, uses lfs instead.
157function updater._mkdir(path)
158 _initUpdater()
159 return u:createDirectory(path)
160end
161
162-- Departed, get a warning in ios simulator
163function updater._rmdir(path)
164 _initUpdater()
165 return u:removeDirectory(path)
166end
167--]]
168
169function updater.mkdir(path)
170 if not updater.exists(path) then
171 return lfs.mkdir(path)
172 end
173 return true
174end
175
176function updater.rmdir(path)
177 print("updater.rmdir:", path)
178 if updater.exists(path) then
179 local function _rmdir(path)
180 local iter, dir_obj = lfs.dir(path)
181 while true do
182 local dir = iter(dir_obj)
183 if dir == nil then break end
184 if dir ~= "." and dir ~= ".." then
185 local curDir = path..dir
186 local mode = lfs.attributes(curDir, "mode")
187 if mode == "directory" then
188 _rmdir(curDir.."/")
189 elseif mode == "file" then
190 os.remove(curDir)
191 end
192 end
193 end
194 local succ, des = os.remove(path)
195 if des then print(des) end
196 return succ
197 end
198 _rmdir(path)
199 end
200 return true
201end
202
203-- Is there a update.zip package in ures directory?
204-- If it is true, return its abstract path.
205function updater.hasNewUpdatePackage()
206 local newUpdater = ures.."lib/update.zip"
207 if updater.exists(newUpdater) then
208 return newUpdater
209 end
210 return nil
211end
212
213-- Check local resinfo and remote resinfo, compare their version value.
214function updater.checkUpdate()
215 localResInfo = updater.getLocalResInfo()
216 local localVer = localResInfo.version
217 print("localVer:", localVer)
218 remoteResInfo = updater.getRemoteResInfo(localResInfo.update_url)
219 local remoteVer = remoteResInfo.version
220 print("remoteVer:", remoteVer)
221 return remoteVer ~= localVer
222end
223
224-- Copy resinfo.lua from original package to update directory(ures)
225-- when it is not in ures.
226function updater.getLocalResInfo()
227 print(string.format("updater.getLocalResInfo, lresinfo:%s, uresinfo:%s",
228 lresinfo,uresinfo))
229 local resInfoTxt = nil
230 if updater.exists(uresinfo) then
231 resInfoTxt = updater.readFile(uresinfo)
232 else
233 assert(updater.mkdir(ures), ures.." create error!")
234 local info = updater.readFile(lresinfo)
235 print("localResInfo:", info)
236 assert(info, string.format("Can not get the constent from %s!", lresinfo))
237 updater.writeFile(uresinfo, info)
238 resInfoTxt = info
239 end
240 return assert(loadstring(resInfoTxt))()
241end
242
243function updater.getRemoteResInfo(path)
244 _initUpdater()
245 print("updater.getRemoteResInfo:", path)
246 local resInfoTxt = u:getUpdateInfo(path)
247 print("resInfoTxt:", resInfoTxt)
248 return assert(loadstring(resInfoTxt))()
249end
250
251function updater.update(handler)
252 assert(remoteResInfo and remoteResInfo.package, "Can not get remoteResInfo!")
253 print("updater.update:", remoteResInfo.package)
254 if handler then
255 u:registerScriptHandler(handler)
256 end
257 updater.rmdir(utmp)
258 u:update(remoteResInfo.package, uzip, utmp, false)
259end
260
261function updater._copyNewFile(resInZip)
262 -- Create nonexistent directory in update res.
263 local i,j = 1,1
264 while true do
265 j = string.find(resInZip, "/", i)
266 if j == nil then break end
267 local dir = string.sub(resInZip, 1,j)
268 -- Save created directory flag to a table because
269 -- the io operation is too slow.
270 if not updater._dirList[dir] then
271 updater._dirList[dir] = true
272 local fullUDir = uroot..dir
273 updater.mkdir(fullUDir)
274 end
275 i = j+1
276 end
277 local fullFileInURes = uroot..resInZip
278 local fullFileInUTmp = utmp..resInZip
279 print(string.format('copy %s to %s', fullFileInUTmp, fullFileInURes))
280 local zipFileContent = updater.readFile(fullFileInUTmp)
281 if zipFileContent then
282 updater.writeFile(fullFileInURes, zipFileContent)
283 return fullFileInURes
284 end
285 return nil
286end
287
288function updater._copyNewFilesBatch(resType, resInfoInZip)
289 local resList = resInfoInZip[resType]
290 if not resList then return end
291 local finalRes = finalResInfo[resType]
292 for __,v in ipairs(resList) do
293 local fullFileInURes = updater._copyNewFile(v)
294 if fullFileInURes then
295 -- Update key and file in the finalResInfo
296 -- Ignores the update package because it has been in memory.
297 if v ~= "res/lib/update.zip" then
298 finalRes[v] = fullFileInURes
299 end
300 else
301 print(string.format("updater ERROR, copy file %s.", v))
302 end
303 end
304end
305
306function updater.updateFinalResInfo()
307 assert(localResInfo and remoteResInfo,
308 "Perform updater.checkUpdate() first!")
309 if not finalResInfo then
310 finalResInfo = updater.clone(localResInfo)
311 end
312 --do return end
313 local resInfoTxt = updater.readFile(zresinfo)
314 local zipResInfo = assert(loadstring(resInfoTxt))()
315 if zipResInfo["version"] then
316 finalResInfo.version = zipResInfo["version"]
317 end
318 -- Save a dir list maked.
319 updater._dirList = {}
320 updater._copyNewFilesBatch("lib", zipResInfo)
321 updater._copyNewFilesBatch("oth", zipResInfo)
322 -- Clean dir list.
323 updater._dirList = nil
324 updater.rmdir(utmp)
325 local dumpTable = updater.vardump(finalResInfo, "local data", true)
326 dumpTable[#dumpTable+1] = "return data"
327 if updater.writeFile(uresinfo, table.concat(dumpTable, "\n")) then
328 return true
329 end
330 print(string.format("updater ERROR, write file %s.", uresinfo))
331 return false
332end
333
334function updater.getResCopy()
335 if finalResInfo then return updater.clone(finalResInfo) end
336 return updater.clone(localResInfo)
337end
338
339function updater.clean()
340 if u then
341 u:unregisterScriptHandler()
342 u:delete()
343 u = nil
344 end
345 updater.rmdir(utmp)
346 localResInfo = nil
347 remoteResInfo = nil
348 finalResInfo = nil
349end
350
351return 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(本地索引文件)
安装包内自带的所有资源的索引文件,所有资源路径指向包内自带的资源。打包的时候和产品包一起提供,产品包会默认使用这个资源索引文件来查找资源。它的大概内容如下:
1local data = {
2 version = "1.0",
3 update_url = "http://192.168.18.22:8080/updater/resinfo.lua",
4 lib = {
5 ["res/lib/config.zip"] = "res/lib/config.zip",
6 ["res/lib/framework_precompiled.zip"] = "res/lib/framework_precompiled.zip",
7 ["res/lib/root.zip"] = "res/lib/root.zip",
8 ......
9 },
10 oth = {
11 ["res/pic/init_bg.png"] = "res/pic/init_bg.png",
12 ......
13 },
14}
15return data
从它的结构可以看出,它包含了当前包的版本(version)、在哪里获取要更新的资源索引文件(update_url)、当前包中所有的lua模块的路径(lib)、当前包中所有的资源文件的路径(oth)。
4.5.3.5 uresinfo(更新索引文件)
保存在 ures 中的更新后的索引文件,没有更新的资源路径指向包内自带的资源,更新后的资源路径指向ures中的资源。它的内容大致如下:
config.zip 的路径是在 iOS 模拟器中得到的。
1local data = {
2 version = "1.0",
3 update_url = "http://192.168.18.22:8080/updater/resinfo.lua",
4 lib = {
5 ["res/lib/cc.zip"] = "res/lib/cc.zip",
6 ["res/lib/config.zip"] = "/Users/zrong/Library/Application Support/iPhone Simulator/7.1/Applications/2B46FAC0-C419-42B5-92B0-B06DD16E113B/Documents/res/lib/config.zip",
7 ......
8 },
9 oth = {
10 ["res/pic/init_bg.png"] = "res/pic/init_bg.png",
11 ......
12 },
13}
14return data
4.5.3.6 http://192.168.18.22:8080/updater/resinfo.lua
getRemoteResInfo
方法会读取这个文件,然后将结果解析成lua table。对比其中的version与 lrefinfo 中的区别,来决定是否需要更新。
若需要,则调用C++ Updater模块中的方法下载 package 指定的zip包并解压。
它的内容如下:
1local data = {
2 version = "1.0.2",
3 package = "http://192.168.18.22:8080/updater/res.zip",
4}
5return 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 ,它用于指示哪些文件需要更新。内容大致如下:
1local data = {
2 version = "1.0.2",
3 lib = {
4 "res/lib/cc.zip",
5 ......
6 },
7 oth = {
8 "res/pic/init_bg",
9 ......
10 },
11}
12return data
这个文件中包含的所有文件必须能在zip解压后找到。
4.5.4 update.updater.updateFinalResInfo()
这是一个至关重要的方法,让我们代入用上面提到的变量名和目录来描述它的功能:
它实现的功能是:
- 读取 uresinfo,若没有,则将 lresinfo 复制成 uresinfo;
- 从 utmp 中读取 zresinfo,注意此时zip文件已经解压;
- 将需要更新的资源文件从 utmp 中复制到 ures 中;
- 更新 uresinfo ,使其中的资源键名指向正确的资源路径(上一步复制的目标路径);
- 删除 utmp;
- 将更新后的 uresinfo 作为lua文件写入 ures 。
4.5.5 其它方法
对 update.updater 的调用一般是这样的顺序:
- 调用 checkUpdat 方法检测是否需要升级;
- 调用 update 方法执行升级,同时注册事件管理handler;
- 升级成功,调用 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
在程序中,我们一般会使用这样的写法来获取资源:
1display.newSprite("pic/init_bg.png")
或者干脆简化成了:
1display.newSprite("init_bg.png")
要上面的代码能够工作,需要为 CCFileUtils 设置搜索路径:
1CCFileUtils:sharedFileUtils:addSearchPath("res/")
2CCFileUtils:sharedFileUtils:addSearchPath("res/pic/")
但是,在这套更新机制中,我不建议设置搜索路径,因为素材都是以完整路径格式保存的,这样使用起来更方便和更确定。
如果是新项目,那么挺好,我只需要保证素材路径基于 res
提供即可,类似这样:
1display.newSprite("res/pic/init_bg.png")
但是对于已经开发了一段时间的项目来说,一个个改就太不专业了。这是我们需要扩展一个 io.getres 方法:
1res = {}
2
3function io.getres(path)
4 print("io.getres originl:", path)
5 if CCFileUtils:sharedFileUtils():isAbsolutePath(path) then
6 return path
7 end
8 if res[path] then return res[path] end
9 for key, value in pairs(finalRes.oth) do
10 print(key, value)
11 local pathInIndex = string.find(key, path)
12 if pathInIndex and pathInIndex >= 1 then
13 print("io.getres getvalue:", path)
14 res[path] = value
15 return value
16 end
17 end
18 print("io.getres no get:", path)
19 return path
20end
然后,我们需要修改 quick framework 中的display模块让我们的旧代码不必进行任何改动就能生效。
5.2 修改 display.newSprite
找到该方法中的这个部分:
1if string.byte(filename) == 35 then -- first char is #
2 local frame = display.newSpriteFrame(string.sub(filename, 2))
3 if frame then
4 sprite = spriteClass:createWithSpriteFrame(frame)
5 end
6else
7 if display.TEXTURES_PIXEL_FORMAT[filename] then
8 CCTexture2D:setDefaultAlphaPixelFormat(display.TEXTURES_PIXEL_FORMAT[filename])
9 sprite = spriteClass:create(filename)
10 CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA8888)
11 else
12 sprite = spriteClass:create(filename)
13 end
14end
将其改为:
1if string.byte(filename) == 35 then -- first char is #
2 local frame = display.newSpriteFrame(string.sub(filename, 2))
3 if frame then
4 sprite = spriteClass:createWithSpriteFrame(frame)
5 end
6else
7 local absfilename = io.getres(filename)
8 if display.TEXTURES_PIXEL_FORMAT[filename] then
9 CCTexture2D:setDefaultAlphaPixelFormat(display.TEXTURES_PIXEL_FORMAT[filename])
10 sprite = spriteClass:create(absfilename)
11 CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA8888)
12 else
13 sprite = spriteClass:create(absfilename)
14 end
15end
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分钟够么?**
- 文章ID:2131
- 原文作者:zrong
- 原文链接:https://blog.zengrong.net/post/hot_update_in_quick-cocos2d-x/
- 版权声明:本作品采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可,非商业转载请注明出处(原文作者,原文链接),商业转载请联系作者获得授权。