quick-cocos2d-x的熱更新機制實現update包(lua)(上)

原文地址: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 怎麼使用這個模塊
你如果要直接拿來就用,這個模塊基本上不需要修改。因爲本來它就沒什麼特別的功能。當然,你可以看完下面兩個模塊再決定。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章