原理:
每次登陸游戲利用cocos的assetManager從服務(wù)器拉去當(dāng)前最新的兩個文件嘹吨。 一個是version.mainifest,一個project.mainifest. 這兩個文件都是xml的描述文件。一個包含了版本信息沿猜,第二個包含了游戲所有資源的MD5碼。首先通過version文件對比本地的版本是否相同夯到,如果不相同房铭,再通過跟本地的project文件對比MD5碼來判斷哪些文件需要重新下載,替換資源载慈。
步驟:
1. 有一個文件下載的熱更新服務(wù)器,將最新項目資源(res/ src/ 目錄)放入熱更新服務(wù)器中珍手,添加版本信息母文件(version_info.json)和python腳本文件eneateManifest.py(生成project.manifest办铡、version.manifest文件)。
2.version_info.json文件: 主要用來配置信息
{
"packageUrl" : "http://ip:port/update/MyProj/assets/",
"remoteManifestUrl" : "http://ip:port/update/MyProj/version/project.manifest",
"remoteVersionUrl" : "http://ip:port/update/MyProj/version/version.manifest",
"engineVersion" : "3.3",
"update_channel" : "Android",
"bundle" : "2018111701",
"version" : "1.0.0",
}
3.eneateManifest.py文件: 這個文件是一個python琳要。目的是生成對應(yīng)的version和project文件寡具。project文件可以幫你給每個資源生成獨一無二的MD5碼,相當(dāng)于每個資源的標(biāo)記稚补。下面是一段python文件的代碼童叠。
#coding:utf-8
import os
import sys
import json
import hashlib
import subprocess
import getpass
username = getpass.getuser()
# 改變當(dāng)前工作目錄
#os.chdir('/Users/' + username + '/Documents/client/MyProj/')
assetsDir = {
#MyProj文件夾下需要進(jìn)行熱跟的文件夾
"searchDir" : ["src", "res"],
#需要忽略的文件夾
"ignorDir" : ["cocos", "framework", ".svn"],
#需要忽略的文件
"ignorFile":[".DS_Store"],
}
versionConfigFile = "version/version_info.json" #版本信息的配置文件路徑
versionManifestPath = "version/version.manifest" #由此腳本生成的version.manifest文件路徑
projectManifestPath = "version/project.manifest" #由此腳本生成的project.manifest文件路徑
# projectManifestPath = "/Users/ximi/Documents/client/MyProj/res/version/project.manifest" #由此腳本生成的project.manifest文件路徑(mac機)
class SearchFile:
def __init__(self):
self.fileList = []
for k in assetsDir:
if (k == "searchDir"):
for searchdire in assetsDir[k]:
self.recursiveDir(searchdire)
def recursiveDir(self, srcPath):
''' 遞歸指定目錄下的所有文件'''
dirList = [] #所有文件夾
files = os.listdir(srcPath) #返回指定目錄下的所有文件,及目錄(不含子目錄)
for f in files:
#目錄的處理
if (os.path.isdir(srcPath + '/' + f)):
if (f[0] == '.' or (f in assetsDir["ignorDir"])):
#排除隱藏文件夾和忽略的目錄
pass
else:
#添加非需要的文件夾
dirList.append(f)
#文件的處理
elif (os.path.isfile(srcPath + '/' + f)) and (f not in assetsDir["ignorFile"]):
self.fileList.append(srcPath + '/' + f) #添加文件
#遍歷所有子目錄,并遞歸
for dire in dirList:
#遞歸目錄下的文件
self.recursiveDir(srcPath + '/' + dire)
def getAllFile(self):
''' get all file path'''
return tuple(self.fileList)
def CalcMD5(filepath):
"""generate a md5 code by a file path"""
with open(filepath,'rb') as f:
md5obj = hashlib.md5()
md5obj.update(f.read())
return md5obj.hexdigest()
def getVersionInfo():
'''get version config data'''
configFile = open(versionConfigFile,"r")
json_data = json.load(configFile)
configFile.close()
# json_data["version"] = json_data["version"] + '.' + str(GetSvnCurrentVersion())
json_data["version"] = json_data["version"]
return json_data
def GenerateVersionManifestFile():
''' 生成大版本的version.manifest'''
json_str = json.dumps(getVersionInfo(), indent = 2)
fo = open(versionManifestPath,"w")
fo.write(json_str)
fo.close()
def GenerateProjectManifestFile():
searchfile = SearchFile()
fileList = list(searchfile.getAllFile())
project_str = {}
project_str.update(getVersionInfo())
dataDic = {}
for f in fileList:
dataDic[f] = {"md5" : CalcMD5(f)}
print f
project_str.update({"assets":dataDic})
json_str = json.dumps(project_str, sort_keys = True, indent = 2)
fo = open(projectManifestPath,"w")
fo.write(json_str)
fo.close()
if __name__ == "__main__":
GenerateVersionManifestFile()
GenerateProjectManifestFile()
生成version.manifest如下
{
"packageUrl": "http://ip:port/update/MyProj/assets/",
"engineVersion": "3.3",
"version": "1.0.0",
"remoteVersionUrl": "http://ip:port/update/MyProj/version/version.manifest",
"remoteManifestUrl": "http://ip:port/update/MyProj/version/project.manifest"
}
生成project.manifest如下
{
"assets": {
"src/packages/mvc/init.lua": {
"md5": "6b9173481a1300c5e737ad5885ebef00"
},
"src/protobuf.lua": {
"md5": "f790fe35eb179a4341ff41d94e488a5d"
}
...
},
"packageUrl": "http://ip:port/update/MyProj/assets/",
"engineVersion": "3.3",
"version": "1.0.0",
"remoteVersionUrl": "http://ip:port/update/MyProj/version/version.manifest",
"remoteManifestUrl": "http://ip:port/update/MyProj/version/project.manifest"
}
4.游戲客戶端: 利用cocos assetManager來從服務(wù)器獲取文件并且進(jìn)行資源的替換(這里所謂的替換并不是真正的替換孔厉,利用了Fileutils->searchPath() 設(shè)置資源文件讀取的優(yōu)先級。也就是老資源和代碼并沒有刪除帖努,而是舍棄不用撰豺。
--region *.lua
--Date
local AssetsManager = class("AssetsManager",function ()
return cc.LayerColor:create(cc.c4b(20, 20, 20, 220))
end)
function AssetsManager:ctor()
self:onNodeEvent("exit", handler(self, self.onExitCallback))
self:initUI()
self:setAssetsManage()
end
function AssetsManager:onExitCallback()
self.assetsManagerEx:release()
end
function AssetsManager:initUI()
local hintLabel = cc.Label:createWithTTF("正在更新...", CONFIG.TTF_FONT_2, 20)
:addTo(self)
:move(600, 80)
local progressBg = display.newSprite("sprites/hyd_progress_bg.png")
:addTo(self)
:move(600, 40)
self.progress = cc.ProgressTimer:create(display.newSprite("sprites/hyd_progress.png"))
:addTo(progressBg)
:move(380, 19)
self.progress:setType(cc.PROGRESS_TIMER_TYPE_BAR)
self.progress:setBarChangeRate(cc.p(1, 0))
self.progress:setMidpoint(cc.p(0.0, 0.5))
self.progress:setPercentage(0)
--觸摸吞噬
self.listener = cc.EventListenerTouchOneByOne:create()
self.listener:setSwallowTouches(true)
local onTouchBegan = function (touch, event)
return true
end
self.listener:registerScriptHandler(onTouchBegan, cc.Handler.EVENT_TOUCH_BEGAN)
cc.Director:getInstance():getEventDispatcher():addEventListenerWithSceneGraphPriority(self.listener, self)
end
function AssetsManager:setAssetsManage()
--創(chuàng)建可寫目錄與設(shè)置搜索路徑
local storagePath = cc.FileUtils:getInstance():getWritablePath() .. "NewRes/"
local resPath = storagePath.. '/res/'
local srcPath = storagePath.. '/src/'
if not (cc.FileUtils:getInstance():isDirectoryExist(storagePath)) then
cc.FileUtils:getInstance():createDirectory(storagePath)
cc.FileUtils:getInstance():createDirectory(resPath)
cc.FileUtils:getInstance():createDirectory(srcPath)
end
local searchPaths = cc.FileUtils:getInstance():getSearchPaths()
table.insert(searchPaths, 1, storagePath)
table.insert(searchPaths, 2, resPath)
table.insert(searchPaths, 3, srcPath)
cc.FileUtils:getInstance():setSearchPaths(searchPaths)
self.assetsManagerEx = cc.AssetsManagerEx:create("version/project.manifest", storagePath)
self.assetsManagerEx:retain()
local eventListenerAssetsManagerEx = cc.EventListenerAssetsManagerEx:create(self.assetsManagerEx,
function (event)
self:handleAssetsManagerEvent(event)
end)
local dispatcher = cc.Director:getInstance():getEventDispatcher()
dispatcher:addEventListenerWithFixedPriority(eventListenerAssetsManagerEx, 1)
--檢查版本并升級
self.assetsManagerEx:update()
end
function AssetsManager:handleAssetsManagerEvent(event)
local eventCodeList = cc.EventAssetsManagerEx.EventCode
local eventCodeHand = {
[eventCodeList.ERROR_NO_LOCAL_MANIFEST] = function ()
print("發(fā)生錯誤:本地資源清單文件未找到")
end,
[eventCodeList.ERROR_DOWNLOAD_MANIFEST] = function ()
print("發(fā)生錯誤:遠(yuǎn)程資源清單文件下載失敗") --資源服務(wù)器沒有打開,
self:downloadManifestError()
end,
[eventCodeList.ERROR_PARSE_MANIFEST] = function ()
print("發(fā)生錯誤:資源清單文件解析失敗")
end,
[eventCodeList.NEW_VERSION_FOUND] = function ()
print("發(fā)現(xiàn)找到新版本")
end,
[eventCodeList.ALREADY_UP_TO_DATE] = function ()
print("已經(jīng)更新到服務(wù)器最新版本")
self:updateFinished()
end,
[eventCodeList.UPDATE_PROGRESSION]= function ()
print("更新過程的進(jìn)度事件")
self.progress:setPercentage(event:getPercentByFile())
end,
[eventCodeList.ASSET_UPDATED] = function ()
print("單個資源被更新事件")
end,
[eventCodeList.ERROR_UPDATING] = function ()
print("發(fā)生錯誤:更新過程中遇到錯誤")
end,
[eventCodeList.UPDATE_FINISHED] = function ()
print("更新成功事件")
self:updateFinished()
end,
[eventCodeList.UPDATE_FAILED] = function ()
print("更新失敗事件")
end,
[eventCodeList.ERROR_DECOMPRESS] = function ()
print("解壓縮失敗")
end
}
local eventCode = event:getEventCode()
if eventCodeHand[eventCode] ~= nil then
eventCodeHand[eventCode]()
end
end
function AssetsManager:updateFinished()
self:setVisible(false)
self.listener:setEnabled(false)
end
function AssetsManager:downloadManifestError()
self:setVisible(false)
self.listener:setEnabled(false)
end
return AssetsManager
--endregion
Android apk 安裝后在手機中還是以apk存在拼余,apk 不可寫入和刪除污桦,所以熱更新下載的最新資源都存在緩存中,并添加緩存目錄為最高優(yōu)先級搜索目錄匙监,加載資源時從最高優(yōu)先級目錄中加載從而起到替換更新的作用凡橱。
cocos2dx中有一個熱更新類AssetsManagerEx小作,用這個類實現(xiàn)熱更功能時需要有兩個文件,project.manifest以及version.manifest稼钩。這里主要是project.manifest文件
Cocos自身也封裝了熱更新的模塊AssetsManager顾稀、AssetsManagerEx。
AssetsManager采用的是升級包的管理方式坝撑,首先進(jìn)行版本號對比静秆,然后根據(jù)URL獲取對應(yīng)的升級包,解壓升級包巡李,設(shè)置資源加載路徑抚笔,通過加載writepath目錄下最新文件的方式來實現(xiàn)更新。問題是當(dāng)涉及跳版本更新侨拦,或只有一個文件被改動時殊橙,用戶就要下載前面全部的升級內(nèi)容,升級包會越來越大狱从。
AssetsManagerEx是AssetsManager的加強版膨蛮,不同的是不再使用升級包的方式,而是采用單個文件拉取的方式矫夯。首先獲取本地更新配置鸽疾,之后與服務(wù)器的更新配置比對,得出差異文件训貌,之后單個拉取差異文件制肮。當(dāng)本地版本大于服務(wù)器版本時,會清理掉本地更新緩存递沪。AssetsManagerEx也有尚未解決的問題豺鼻,例如多個更新序列無法并行,只能順序啟動款慨。另外版本后期隨著項目龐大配置文件幾乎包含了所有的文件信息儒飒,對比文件時間的耗時會越來越長。