概述本站文章除注明转载外,均为本站原创或者翻译。 本站文章欢迎各种形式的转载,但请18岁以上的转载者注明文章出处,尊重我的劳动,也尊重你的智商; 本站部分原创和翻译文章提供markdown格式源码,欢迎使用文章源码进行转载; 本文标题:quick-cocos2d-x的
热更新机制实现 本文链接:http://zengrong.net/post/2131.htm 这里说的热更新,指的是客户端的更新。 大致 本站文章除注明转载外,均为本站原创或者翻译。 本站文章欢迎各种形式的转载,但请18岁以上的转载者注明文章出处,尊重我的劳动,也尊重你的智商; 本站部分原创和翻译文章提供markdown格式源码,欢迎使用文章源码进行转载; 本文标题:quick-cocos2d-x的热更新
机制实现 本文链接:http://Zengrong.net/post/2131.htm 这里说的热更新,指的是客户端的更新。 大致的流程是,客户端在启动后访问更新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_@R_560_3013@.zip 模块 为了行文方便,后面会把 frameworks_@R_560_3013@.zip 简称为 framework 。 frameworks 模块是 quick 的核心模块,在quick 生成的项目中,它直接在 AppDelegate.cpp 中载入 main.lua 之前进行载入。如下: 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_@R_560_3013@.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协议下载升级包,需要提供四个参数: zip文件的url; zip文件的保存位置; zip 文件的解压临时目录; 解压之前是否需要清空临时目录。 3.4 修改事件类型 我把把传递给lua的事件分成了四种类型: 3.4.1 UPDATER_MESSAGE_UPDATE_SUCCEED 事件名为 success,代表更新成功,zip文件下载并解压完毕; 3.4.2 UPDATER_MESSAGE_STATE 事件名为 state,更新过程中的状态(下载开始、结束,解压开始、结束)也传递给了lua。这个方法是这样实现的: 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函数的例子。 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 方法,使其变成这样: 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 模块: 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 的内容: --- 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:",__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 函数中至关重要: _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 即可。下面就是核心代码了: local updatePackage = { "update.UpdateApp",} 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_@R_560_3013@.zip 这种)的列表。我们通过循环将它们载入内存。 对于 root.RootScene 这个模块来说,就是标准的quick模块了,它可以使用quick中的任何特性。 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 这个模块用于显示更新过程的进度和一些信息。所有内容如下: ------ -- 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,-- top_CENTER CCPoint(1,-- top_RIGHT CCPoint(0,-- CENTER_left CCPoint(1,-- CENTER_RIGHT CCPoint(0,0),-- BottOM_left CCPoint(1,-- BottOM_RIGHT CCPoint(0.5,-- 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)) us._label = __label display.align(__label,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",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中这样写: display.newSprite("res/pic/init_bg.png") :align(display.CENTER,display.cy) :addTo(self,0) 在没有quick framework的时候需要改成这样: local __bg = CCSprite:create(us._getres("res/pic/init_bg.png")) display.align(__bg,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 这是整个更新系统的核心部分了。代码更长一点,但其实很好懂。 在这个模块中,我们需要完成下面的工作: 调用C++的Updater模块来获取远程的版本号以及资源下载地址; 调用C++的Updater模块来下载解压; 合并解压后的新资源到新资源文件夹; 更新总的资源索引; 删除临时文件; 报告更新中的各种错误。 所以说,这是一个工具模块。它提供的是给更新使用的各种工具。而 UpdateApp 和 updateScene 则分别是功能和界面模块。 --- 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,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",_v(object),postfix) else result[#result +1] = string.format("%s%s%s",postfix) end elseif not lookuptable[object] then lookuptable[object] = true if type(label) == "string" then result[#result +1 ] = string.format("%s%s = {",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",postfix) end end _vardump(object,"",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(本地索引文件) 安装包内自带的所有资源的索引文件,所有资源路径指向包内自带的资源。打包的时候和产品包一起提供,产品包会默认使用这个资源索引文件来查找资源。它的大概内容如下: 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_@R_560_3013@.zip"] = "res/lib/framework_@R_560_3013@.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 模拟器中得到的。 local data = { version = "1.0",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",} return data 4.5.3.6 http://192.168.18.22:8080/updater/resinfo.lua getRemoteResInfo 方法会读取这个文件,然后将结果解析成lua table。对比其中的version与 lrefinfo 中的区别,来决定是否需要更新。 若需要,则调用C++ Updater模块中的方法下载 package 指定的zip包并解压。 它的内容如下: 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 ,它用于指示哪些文件需要更新。内容大致如下: 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 在程序中,我们一般会使用这样的写法来获取资源: display.newSprite("pic/init_bg.png") 或者干脆简化成了: display.newSprite("init_bg.png") 要上面的代码能够工作,需要为 CCfileUtils 设置搜索路径: CCfileUtils:sharedfileUtils:addSearchPath("res/") CCfileUtils:sharedfileUtils:addSearchPath("res/pic/") 但是,在这套更新机制中,我不建议设置搜索路径,因为素材都是以完整路径格式保存的,这样使用起来更方便和更确定。 如果是新项目,那么挺好,我只需要保证素材路径基于 res 提供即可,类似这样: display.newSprite("res/pic/init_bg.png") 但是对于已经开发了一段时间的项目来说,一个个改就太不专业了。这是我们需要扩展一个 io.getres 方法: 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,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 找到该方法中的这个部分: 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 将其改为: 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分钟够么? 总结
以上是内存溢出为你收集整理的quick-cocos2d-x的热更新机制实现全部内容,希望文章能够帮你解决quick-cocos2d-x的热更新机制实现所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
评论列表(0条)