0 依赖
这里说的热更新,指的是客户端的更新。
大致的流程是,客户端在启动后访问更新api,根据更新api的反馈,下载更新资源,然后使用新的资源启动客户端,或者直接使用新资源不重启客户端。
这种方式可以跳过AppStore的审核,避免了用户频繁下载、安装、覆盖产品包。
我们一般使用这种方式快速修复产品BUG和增加新功能。
本文基于 。
1 前言
1.1 他山之石
在实现这个机制之前,我研究了这几篇文章:
- by SunLightJuly
- by Henry
- by 西门大官人
另外,我也查看了 的源码和 。
不幸的是,这几个方案我都不能直接拿来用。因此思考再三,还是自己写了一套方案。
==重要提醒==
这篇文章很长,但我不愿意将其分成多个部分。这本来就是一件事,分开的话有种开房时洗完澡妹子却说两个小时后才能来。这中间干点啥呢?
所以,如果你不能坚持两个小时(能么?不能?),或者你的持久度不能坚持到把这篇文章看完(大概要10~30分钟吧),那还是不要往下看的比较好。
当然,你也可能坚挺了30分钟之后才发现妹子是凤姐,不要怪我这30分钟里面没开灯哦……
1.2 为什么要重复造轮子
上面的几个方案侧重于尽量简化用户(使用此方案的程序员)的操作,而简化带来的副作用就是会损失一些灵活性。
正如 Roberto Ierusalimschy 在 第15章开头所说:
通常,Lua不会设置规则(policy)。相反,Lua会提供许多强有力的机制来使开发者有能力实现出最适合的规则。
我认为更新模块也不应该设置规则,而是尽可能提供一些机制来满足程序员的需要。这些机制并不是我发明的,而是Lua和quick本来就提供的。让程序员自己实现自己的升级系统,一定比我这个无证野路子的方法更好.
因此,本文中讲述的并非是一套通用的机制,而是我根据上面说到的这些机制实现的一套适合自己的方法。当然你可以直接拿去用,但要记住:
- 用的好,请告诉你的朋友。
- 出了问题,
请告诉别找我。
1.3 需求的复杂性
热更新有许多的必要条件,每个产品的需求可能都不太相同。
例如,每个产品的版本号设计都不太相同,有的有大版本、小版本;有的则有主版本、次版本、编译版本。我以前的习惯,是在主版本变化的时候需要整包更新,而次版本变化代表逻辑更新,编译版本代表资源更新等等。这些需要自己来定义升级规则。
再例如,有的产品希望逐个下载升级包,有的产品希望把所有资源打包成一个升级包;有的产品直接使用文件名作为资源名在游戏中调用,而有的产品会把资源名改为指纹码(例如MD5)形式来实现升级的多版本共存和实时回滚,还有的产品甚至要求能在用户玩游戏的过程中完成自动更新。
那套机制就太死板,在真实的产品中不修改很难使用。
而我也不建议使用 CCUserDefault
这种东西——在Lua的世界里,为什么要用XML做配置文件?
如果抽象出我的需求,其实只有1点:
能更新一切
这个说的有点大了,准确的说,应该是 能更新一切Lua代码与资源 。
如果你的整个游戏都是Lua写的(对于quick的项目来说应该是这样),其实也就是更新一切。
1.4 版本号设计
关于上面 中提到的版本号的问题,可以参考一下这篇文章: 。
我基于语义化版本设计了一套规则在团队内部使用: 。
在这里,我尽量详细地阐述我的思路和做法,抛砖引玉吧。
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(); // 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的,我基于 的代码实现了一个 Updater 模块。
而且,我还改了 AppDelegate 中的启动代码。
所以,你看,我不仅是撑数,还是忽悠。
3 Updater(C++)
中提供了下载资源,访问更新列表,解压zip包,删除临时文件,设置搜索路径等等一系列的功能。但它的使用方式相当死板,我只能传递一个获取版本号的地址,一个zip包的地址,一个临时文件夹路径,然后就干等着。期间啥也干不了。
当然,我可以通过 quick 为其增加的 registerScriptHandler
方法让lua得知下载进度和网络状态等等。但下载进度的数字居然以事件名的方式通过字符串传过来的!这个就太匪夷所思了点。
于是,我对这个 AssetsManager
进行了修改。因为修改的东西实在太多,改完后就不好意思再叫这个名字了(其实主要是现在的名字比较短 XD)。我们只需要记住这个 Updater
是使用 AssetsManager
修改的即可。
在上面SunLightJuly和Henry同学的方法中,使用的是 CCHTTPRequest
来获取网络资源的。CCHTTPRequest
封装了cURL
操作。而在 Updater
中,是直接封装的 cURL
操作。
在我的设计中,逻辑应该尽量放在lua中,C++部分只提供功能供lua调用。因为lua可以进行热更新,而C++部分则只能整包更新。
主要实现的内容如下:
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。这个方法是这样实现的:
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 中。
详细的机制可以阅读 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)
这个方法已经在 讲过了。注意其中的 _succHandler
是在 update.UpdateApp
中定义的匿名函数。
4.4.5 怎么使用这个模块
如果你要使用这个模块,那么可能大部分都要重写。你可以看到,我在这个模块中只有一个背景图和一个 CCLabeTTF 来显示下载进度和状态。你当然不希望你的更新界面就是这个样子。怎么也得来个妹子做封面不是?
4.5 update.updater
这是整个更新系统的核心部分了。代码更长一点,但其实很好懂。
在这个模块中,我们需要完成下面的工作:
- 调用C++的Updater模块来获取远程的版本号以及资源下载地址;
- 调用C++的Updater模块来下载解压;
- 合并解压后的新资源到新资源文件夹;
- 更新总的资源索引;
- 删除临时文件;
- 报告更新中的各种错误。
所以说,这是一个工具模块。它提供的是给更新使用的各种工具。而 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) 来实现,参见: 。
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.luares/lib/cc.zipres/pic/init_bg.png......
zip文件的下载和解压都是由C++完成的,但是下载和解压的路径需要Lua来提供。这个动作完成后,C++会通知Lua更新成功。Lua会接着进行后续操作就使用下面 中提到的方法来复制资源、合并 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()这是一个至关重要的方法,让我们代入用上面提到的变量名和目录来描述它的功能:
它实现的功能是:
- 读取 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
在程序中,我们一般会使用这样的写法来获取资源:
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 更多的更新方式
我在 里面描述了一些需求,例如:
- 如何回滚更新?
- 如何多个版本共存?
- 如何对资源进行指纹码化?
这些问题都不难解决。方法自己想,我只能写到这儿了。
话说回来,实现了 更新一切 ,你还担心什么呢?