原理:
每次登陸游戲利用cocos的assetManager從服務(wù)器拉去當(dāng)前最新的兩個(gè)文件。 一個(gè)是version.mainifest,一個(gè)project.mainifest. 這兩個(gè)文件都是xml的描述文件。一個(gè)包含了版本信息,第二個(gè)包含了游戲所有資源的MD5碼。首先通過version文件對(duì)比本地的版本是否相同,如果不相同,再通過跟本地的project文件對(duì)比MD5碼來判斷哪些文件需要重新下載,替換資源。
步驟:
1. 有一個(gè)文件下載的熱更新服務(wù)器,將最新項(xiàng)目資源(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文件: 這個(gè)文件是一個(gè)python。目的是生成對(duì)應(yīng)的version和project文件。project文件可以幫你給每個(gè)資源生成獨(dú)一無二的MD5碼,相當(dāng)于每個(gè)資源的標(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機(jī))
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)先級(jí)。也就是老資源和代碼并沒有刪除,而是舍棄不用。
--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)
--檢查版本并升級(jí)
self.assetsManagerEx:update()
end
function AssetsManager:handleAssetsManagerEvent(event)
local eventCodeList = cc.EventAssetsManagerEx.EventCode
local eventCodeHand = {
[eventCodeList.ERROR_NO_LOCAL_MANIFEST] = function ()
print("發(fā)生錯(cuò)誤:本地資源清單文件未找到")
end,
[eventCodeList.ERROR_DOWNLOAD_MANIFEST] = function ()
print("發(fā)生錯(cuò)誤:遠(yuǎn)程資源清單文件下載失敗") --資源服務(wù)器沒有打開,
self:downloadManifestError()
end,
[eventCodeList.ERROR_PARSE_MANIFEST] = function ()
print("發(fā)生錯(cuò)誤:資源清單文件解析失敗")
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("單個(gè)資源被更新事件")
end,
[eventCodeList.ERROR_UPDATING] = function ()
print("發(fā)生錯(cuò)誤:更新過程中遇到錯(cuò)誤")
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 安裝后在手機(jī)中還是以apk存在,apk 不可寫入和刪除,所以熱更新下載的最新資源都存在緩存中,并添加緩存目錄為最高優(yōu)先級(jí)搜索目錄,加載資源時(shí)從最高優(yōu)先級(jí)目錄中加載從而起到替換更新的作用。
cocos2dx中有一個(gè)熱更新類AssetsManagerEx,用這個(gè)類實(shí)現(xiàn)熱更功能時(shí)需要有兩個(gè)文件,project.manifest以及version.manifest。這里主要是project.manifest文件
Cocos自身也封裝了熱更新的模塊AssetsManager、AssetsManagerEx。
AssetsManager采用的是升級(jí)包的管理方式,首先進(jìn)行版本號(hào)對(duì)比,然后根據(jù)URL獲取對(duì)應(yīng)的升級(jí)包,解壓升級(jí)包,設(shè)置資源加載路徑,通過加載writepath目錄下最新文件的方式來實(shí)現(xiàn)更新。問題是當(dāng)涉及跳版本更新,或只有一個(gè)文件被改動(dòng)時(shí),用戶就要下載前面全部的升級(jí)內(nèi)容,升級(jí)包會(huì)越來越大。
AssetsManagerEx是AssetsManager的加強(qiáng)版,不同的是不再使用升級(jí)包的方式,而是采用單個(gè)文件拉取的方式。首先獲取本地更新配置,之后與服務(wù)器的更新配置比對(duì),得出差異文件,之后單個(gè)拉取差異文件。當(dāng)本地版本大于服務(wù)器版本時(shí),會(huì)清理掉本地更新緩存。AssetsManagerEx也有尚未解決的問題,例如多個(gè)更新序列無法并行,只能順序啟動(dòng)。另外版本后期隨著項(xiàng)目龐大配置文件幾乎包含了所有的文件信息,對(duì)比文件時(shí)間的耗時(shí)會(huì)越來越長。