quick-cocos2d-x的热更新机制实现update包(lua)(上)

时间:2022-01-23 22:20:03

原文地址:http://www.cocoachina.com/bbs/read.php?tid=213061

4. update包(lua)


update包是整个项目的入口包,quick会首先载入这个包,甚至在 framework 之前。
4.1 为update包所做的项目修改
我修改了quick项目文件 AppDelegate.cpp 中的 applicationDidFinishLaunching 方法,使其变成这样:

复制代码
  1. bool AppDelegate::applicationDidFinishLaunching()
  2. {
  3.     // initialize director
  4.     CCDirector *pDirector = CCDirector::sharedDirector();
  5.     pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());
  6.     pDirector->setProjection(kCCDirectorProjection2D);
  7.     // set FPS. the default value is 1.0/60 if you don't call this
  8.     pDirector->setAnimationInterval(1.0 / 60);
  9.     // register lua engine
  10.     CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
  11.     CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
  12.     CCLuaStack *pStack = pEngine->getLuaStack();
  13.    
  14.     string gtrackback = "\
  15.     function __G__TRACKBACK__(errorMessage) \
  16.     print(\"----------------------------------------\") \
  17.     print(\"LUA ERROR: \" .. tostring(errorMessage) .. \"\\n\") \
  18.     print(debug.traceback(\"\", 2)) \
  19.     print(\"----------------------------------------\") \
  20.     end";
  21.     pEngine->executeString(gtrackback.c_str());
  22.    
  23.     // load update framework
  24.     pStack->loadChunksFromZIP("res/lib/update.zip");
  25.    
  26.     string start_path = "require(\"update.UpdateApp\").new(\"update\"):run(true)";
  27.     CCLOG("------------------------------------------------");
  28.     CCLOG("EXECUTE LUA STRING: %s", start_path.c_str());
  29.     CCLOG("------------------------------------------------");
  30.     pEngine->executeString(start_path.c_str());
  31.    
  32.     return true;
  33. }


原来位于 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. --- The entry of Game
  2. -- @author zrong(zengrong.net)
  3. -- Creation 2014-07-03
  4. local UpdateApp = {}
  5. UpdateApp.__cname = "UpdateApp"
  6. UpdateApp.__index = UpdateApp
  7. UpdateApp.__ctype = 2
  8. local sharedDirector = CCDirector:sharedDirector()
  9. local sharedFileUtils = CCFileUtils:sharedFileUtils()
  10. local updater = require("update.updater")
  11. function UpdateApp.new(...)
  12.     local instance = setmetatable({}, UpdateApp)
  13.     instance.class = UpdateApp
  14.     instance:ctor(...)
  15.     return instance
  16. end
  17. function UpdateApp:ctor(appName, packageRoot)
  18.     self.name = appName
  19.     self.packageRoot = packageRoot or appName
  20.     print(string.format("UpdateApp.ctor, appName:%s, packageRoot:%s", appName, packageRoot))
  21.     -- set global app
  22.     _G[self.name] = self
  23. end
  24. function UpdateApp:run(checkNewUpdatePackage)
  25.     --print("I am new update package")
  26.     local newUpdatePackage = updater.hasNewUpdatePackage()
  27.     print(string.format("UpdateApp.run(%s), newUpdatePackage:%s",
  28.         checkNewUpdatePackage, newUpdatePackage))
  29.     if  checkNewUpdatePackage and newUpdatePackage then
  30.         self:updateSelf(newUpdatePackage)
  31.     elseif updater.checkUpdate() then
  32.         self:runUpdateScene(function()
  33.             _G["finalRes"] = updater.getResCopy()
  34.             self:runRootScene()
  35.         end)
  36.     else
  37.         _G["finalRes"] = updater.getResCopy()
  38.         self:runRootScene()
  39.     end
  40. end
  41. -- Remove update package, load new update package and run it.
  42. function UpdateApp:updateSelf(newUpdatePackage)
  43.     print("UpdateApp.updateSelf ", newUpdatePackage)
  44.     local updatePackage = {
  45.         "update.UpdateApp",
  46.         "update.updater",
  47.         "update.updateScene",
  48.     }
  49.     self:_printPackages("--before clean")
  50.     for __,v in ipairs(updatePackage) do
  51.         package.preload[v] = nil
  52.         package.loaded[v] = nil
  53.     end
  54.     self:_printPackages("--after clean")
  55.     _G["update"] = nil
  56.     CCLuaLoadChunksFromZIP(newUpdatePackage)
  57.     self:_printPackages("--after CCLuaLoadChunksForZIP")
  58.     require("update.UpdateApp").new("update"):run(false)
  59.     self:_printPackages("--after require and run")
  60. end
  61. -- Show a scene for update.
  62. function UpdateApp:runUpdateScene(handler)
  63.     self:enterScene(require("update.updateScene").addListener(handler))
  64. end
  65. -- Load all of packages(except update package, it is not in finalRes.lib)
  66. -- and run root app.
  67. function UpdateApp:runRootScene()
  68.     for __, v in pairs(finalRes.lib) do
  69.         print("runRootScene:CCLuaLoadChunksFromZip",__, v)
  70.         CCLuaLoadChunksFromZIP(v)
  71.     end
  72.    
  73.     require("root.RootScene").new("root"):run()
  74. end
  75. function UpdateApp:_printPackages(label)
  76.     label = label or ""
  77.     print("\npring packages "..label.."------------------")
  78.     for __k, __v in pairs(package.preload) do
  79.         print("package.preload:", __k, __v)
  80.     end
  81.     for __k, __v in pairs(package.loaded) do
  82.         print("package.loaded:", __k, __v)
  83.     end
  84.     print("print packages "..label.."------------------\n")
  85. end
  86. function UpdateApp:exit()
  87.     sharedDirector:endToLua()
  88.     os.exit()
  89. end
  90. function UpdateApp:enterScene(__scene)
  91.     if sharedDirector:getRunningScene() then
  92.         sharedDirector:replaceScene(__scene)
  93.     else
  94.         sharedDirector:runWithScene(__scene)
  95.     end
  96. end
  97. return UpdateApp



我来说几个重点。
4.3.1 没有framework


由于没有加载 framework,class当然是不能用的。所有quick framework 提供的方法都不能使用。
我借用class中的一些代码来实现 UpdateApp 的继承。其实我觉得这个UpdateApp也可以不必写成class的。


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


run 是入口函数,同时接受一个参数,这个参数用于判断是否要检测本地有新的 update.zip 模块。
是的,run 就是那个在 AppDelegate.cpp 中第一个调用的lua函数。
这个函数接受一个参数 checkNewUpdatePackage ,在C++调用 run 的时候,传递的值是 true 。
如果这个值为真,则会检测本地是否拥有新的更新模块,这个检测通过 update.updater.hasNewUpdatePackage() 方法进行,后面会说到这个方法。
本地有更新的 update 模块,则直接调用 updateSelf 来更新 update 模块自身;若无则检测是否有项目更新,下载更新的资源,解析它们,处理它们,然后启动主项目。这些工作通过 update.updater.checkUpdate() 完成,后面会说到这个方法。
若没有任何内容需要更新,则直接调用 runRootScene 来显示主场景了。这后面的内容就交给住场景去做了,update 模块退出历史舞台。


从上面这个流程可以看出。在更新完成之前,主要的项目代码和资源没有进行任何载入。这也就大致达到了我们  更新一切  的需求。因为所有的东西都没有载入,也就不存在更新。只需要保证我载入的内容是最新的就行了。
因此,只要保证 update 模块能更新,就达到我们最开始的目标了。
这个流程还可以保证,如果没有更新,甚至根本就不需要载入 update 模块的场景界面,直接跳转到游戏的主场景即可。


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

复制代码
  1. _G["finalRes"] = updater.getResCopy()



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


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


这是本套机制中最重要的一环。理解了它,你就知道更新一切其实没什么秘密。Lua本来就提供了这样一套机制。
由于在 C++ 中已经将 update 模块载入了内存,那么要更新自身首先要做的是清除 Lua 的载入标记。
Lua在两个全局变量中做了标记:
  • package.preload 执行 CCLuaLoadChunksFromZIP 之后会将模块缓存在这里作为 require 的加载器;
  • package.loaded 执行 require 的时候会先查询 package.loaded,若没有则会查询 package.preload 得到加载器,利用加载器加载模块,再将加载的模块缓存到 package.loaded 中。
详细的机制可以阅读  Lua程序设计(第2版) 15.1 require 函数。
那么,要更新自己,只需要把 package.preload 和 package.loaded 清除,然后再用新的 模块填充 package.preload 即可。下面就是核心代码了:



复制代码
  1. local updatePackage = {
  2.     "update.UpdateApp",
  3.     "update.updater",
  4.     "update.updateScene",
  5. }
  6. for __,v in ipairs(updatePackage) do
  7.     package.preload[v] = nil
  8.     package.loaded[v] = nil
  9. end
  10. _G["update"] = nil
  11. CCLuaLoadChunksFromZIP(newUpdatePackage)
  12. 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. for __, v in pairs(finalRes.lib) do
  2.     print("runRootScene:CCLuaLoadChunksFromZip",__, v)
  3.     CCLuaLoadChunksFromZIP(v)
  4. end
  5. require("root.RootScene").new("root"):run()



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