quick-cocos2d-x的熱更新機制實現
本站文章除註明轉載外,均爲本站原創或者翻譯。
本站文章歡迎各種形式的轉載,但請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_precompiled.zip 模塊
爲了行文方便,後面會把 frameworks_precompiled.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_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的,我基於 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:", __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.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, 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中這樣寫:
display.newSprite("res/pic/init_bg.png")
:align(display.CENTER, display.cx, display.cy)
:addTo(self, 0)
在沒有quick framework的時候需要改成這樣:
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)
這個方法已經在 上面 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, 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) 來實現,參見:在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_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 模擬器中得到的。
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包並解壓。
它的內容如下:
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, 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
找到該方法中的這個部分:
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分鐘夠麼?
本站文章歡迎各種形式的轉載,但請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_precompiled.zip 模塊
爲了行文方便,後面會把 frameworks_precompiled.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_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的,我基於 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:", __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.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, 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中這樣寫:
display.newSprite("res/pic/init_bg.png")
:align(display.CENTER, display.cx, display.cy)
:addTo(self, 0)
在沒有quick framework的時候需要改成這樣:
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)
這個方法已經在 上面 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, 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) 來實現,參見:在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_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 模擬器中得到的。
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包並解壓。
它的內容如下:
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, 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
找到該方法中的這個部分:
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分鐘夠麼?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.