原文地址:http://www.cocoachina.com/bbs/read.php?tid=213061
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:", __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 函数中至关重要:
复制代码
- _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",
- "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中的任何特性。
复制代码
- for __, v in pairs(finalRes.lib) do
- print("runRootScene:CCLuaLoadChunksFromZip",__, v)
- CCLuaLoadChunksFromZIP(v)
- end
-
- require("root.RootScene").new("root"):run()
|
4.3.5 怎么使用这个模块
你如果要直接拿来就用,这个模块基本上不需要修改。因为本来它就没什么特别的功能。当然,你可以看完下面两个模块再决定。