版本:2.4.2
參考:
csdn:Cocos Creator 熱更新(動態修改熱更地址)
demo下載:
這里用cocos2.4.2版本,從零實現android熱更新,從1.0.0版本熱更到2.0.0版本的demo操作流程。從而了解熱更新的基本環境搭建和原理。
一 看官方教程
二 從零新建項目,實現android熱更新
三 版本從1.0.0熱更新到2.0.0
四 遇到的問題
五 動態熱更
六 強更新
一 看官方教程
首先瀏覽下官方的教程熱更新范例教程和熱更新管理器。知道熱更新大概的原理流程。
更新流程大致如下:
- 基於原生打包目錄中的 assets 和 src 目錄生成本地 Manifest 文件。
- 創建一個熱更新組件來負責熱更新邏輯。
- 游戲發布后,若需要更新版本,則生成一套遠程版本資源,包含 assets 目錄、src 目錄和 Manifest 文件,將遠程版本部署到服務端。
- 當熱更新組件檢測到服務端 Manifest 版本不一致時,就會開始熱更新
本地構建發布android文件和熱更新遠程文件如下:
assets和src:代碼和圖片等資源
project.manifest:版本配置文件
version.manifest:project.manifest的一部分,因為project.manifest保存了資源配置導致過大,每次加載影響體驗,所以抽離了遠程資源地址和版本號到version.manifest中,以方便快速加載比對版本。
project.manifiest文件:
version.manifest
二 從零新建項目,實現android熱更新
1. 搭建原生開發環境,新建空項目,並構建-編譯-運行android。
2. 下載官網Demo
下載地址:https://github.com/cocos-creator/tutorial-hot-update
下載下來就有了疑問,demo是什么年代的版本?都幾年幾個月沒更新了,要配置什么環境怎么樣才能跑起來?要用到當下新版本里要怎么修改?
api過時了怎么辦?跑起來各種報錯怎么整?我用的TS但是demo是JS怎么辦?
所以demo我就不跑了,新建一個項目跑。
3. 復制熱更新插件
新建一個TS項目后,復制demo里的packages到新項目中相同位置。packages里是熱更新插件,會在構建項目時,在main.js里插入一段熱更新相關代碼。
4. 本地搭建遠程資源目錄
在新項目中創建remote-assets文件夾,用於存放遠程版本資源。
安裝python2.7.5+版本,我用的是2.7.13,在新項目中創建一個python_server.bat文件
文件內容如下:
python -m SimpleHTTPServer 8000
雙擊bat,相當於以新項目為根目錄搭建了一個簡易服務器。
瀏覽器輸入如下,則相當於訪問新項目遠程資源remote-assets文件夾 (192.168.0.60替換成你電腦本地IP)
http://192.168.0.60:8000/remote-assets/
5. 復制並修改熱更新組件
從demo中找到HotUpdate.js,cocos提供的是js的,里面是熱更新UI和邏輯的代碼,我新項目用的是ts。
把demo的HotUpdate.js的代碼復制到新項目的HotUpdate.ts里,並做相應的修改。
修改1:
demo里的版本文件ManifestStr是直接寫死在HotUpdate.js里,這肯定是不行的。
修改將customManifestStr值從版本文件manifest獲取
修改2:
cc.loader已經過時,將兩處使用cc.loader轉換md5的地方都注釋掉。
修改3:
將demo里用到的組件UI和變量都重新寫過,新項目用的UI丑點無所謂,組件重命名了也沒關系,反正只要和官方demo的匹配上就行。
另外的修改可以參考Demo源碼,把HotUpdate.ts組件掛到新項目場景組件上,這樣新項目加載場景后就可以執行熱更新了。
6. 復制版本生成器
復制version_generator.js到新項目中相同位置,這個版本生成文件會生成當前版本的project.manifest和version.manifest配置文件。
新項目中新建熱更新.bat文件,這個用於執行version_generator.js。
熱更新.bat內容如下:
@ECHO OFF @node version_generator.js -v 1.0.0 -u http://192.168.0.60:8000/remote-assets/ -s build/jsb-link/ -d assets/ pause
參數說明:
-v
指定 Manifest 文件的主版本號。-u
指定服務器遠程包的地址,這個地址需要和最初發布版本中 Manifest 文件的遠程包地址一致,否則無法檢測到更新。-s
本地原生打包版本的目錄相對路徑。-d
保存 Manifest 文件的地址。
總結下新項目現在都做了什么:
1. 新建項目,並能夠正常構建-編譯-運行,在真機上可以跑起來這個空項目。
2. 下載官方熱更新demo
3. 復制了熱更新插件package
4. 本地搭建了遠程版本資源目錄remote-assets
5. 熱更新組件HotUpdate.js改成了ts
6. 復制了版本生成器version_generator.js
做完以上的操作,基本環境就准備好了。
三 版本從1.0.0熱更新到2.0.0
大致操作流程如下:
1. 構建2.0.0項目
2. 將2.0.0項目資源和版本配置放到遠程資源文件夾remote-assets
3. 構建1.0.0項目
4. 運行1.0.0項目,熱更新到2.0.0
1. 構建2.0.0項目
發布原生不需要勾選md5
2.執行熱更新.bat
將熱更新.bat的-v修改成2.0.0,雙擊執行。這是根據當前2.0.0項目,生成了2.0.0版本文件project.manifest和version.manifest,將這兩個文件復制放到resource下。
在MainScene上掛載HotUpdate.ts組件,並將project.manifest拖動賦值給組件HotUpdate的manifestUrl屬性。
3. 再次構建項目
這是將上一步生成的project.manifest和version.manifest打包進去。將project.manifest和version.manifest復制到remote-assets下
將build/js-link下assets和src文件夾復制到remote-assets下
這樣就在remote-assets中有個2.0.0版本的熱更包
4. 修改場景,構建項目
這是構建1.0.0版本項目,任意修改項目場景,比如增加一個圖片,增加一個label文本等,能看出來和2.0.0不一樣。
5. 執行熱更新.bat
將熱更新.bat的-v修改成1.0.0,雙擊執行,這是根據項目生成1.0.0版本的project.manifest和version.manifest。
將1.0.0版本的這兩個文件覆蓋掉項目resouces下2.0.0版本的。
6. 構建項目
這是將上一步生成的1.0.0版本文件project.manifest和version.manifest打包進去
7. androd studio 真機運行項目(也可以cocos模擬器先跑跑)
真機運行時,是1.0.0版本
點擊檢查更新,提示有新版本
點擊立即更新,則更新到2.0.0版本
四 遇到的問題
1. 發布原生不要勾選md5 Cache
發布原生不需要勾選md5,勾選了以后熱更新會找不到文件。
例如不勾選md5時發布文件名為abc.json,勾選md5后文件名變成abc.aaa.json,會多出一串字符。但是熱更新仍然會去找abc.json,導致找不到。
2. 真機每次測試需要刪除一次app
運行項目時,需要刪除手機上app,這樣才能刪除原apk的版本文件緩存.
3.熱更能從1.0.0更新到2.0.0,能不能從2.0.0回退到1.0.0?
官方提供的版本比較函數,只有服務端版本>客戶端版本時,才會進行更新。所以不能從2.0.0回退到1.0.0版本。
/** * 版本對比 * @param versionA 客戶端版本 * @param versionB 服務端版本 * @returns 客戶端>服務端返回正數; 客戶端=服務端返回0; 客戶端<服務端返回負數 */ private versionCompareHandle (versionA, versionB) { console.log("[HotUpdate] JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB); var vA = versionA.split('.'); var vB = versionB.split('.'); for (var i = 0; i < vA.length; ++i) { var a = parseInt(vA[i]); var b = parseInt(vB[i] || 0); if (a === b) { continue; } else { return a - b; } } if (vB.length > vA.length) { return -1; } else { return 0; } };
需要修改如下,只要服務端和客戶端版本號不一致,就進行更新,這樣可以進行版本回退。
/** * 版本對比 * @param versionA 客戶端版本 * @param versionB 服務端版本 * @returns 客戶端>服務端返回正數; 客戶端=服務端返回0; 客戶端<服務端返回負數 */ private versionCompareHandle (versionA, versionB) { console.log("[HotUpdate] JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB); var vA = versionA.split('.'); var vB = versionB.split('.'); //長度不相等,則進行更新 if(vA.length != vB.length){ return -1; } for (var i = 0; i < vA.length; ++i) { var a = parseInt(vA[i]); var b = parseInt(vB[i] || 0); //數字相同,則跳過 if (a === b) { continue; //數字不同,則進行更新 }else { return -1; } } //長度相等且數字相等,則不更新 return 0; };
五 動態更新
為什么需要動態熱更地址?
cocos的熱更新地址是放在project.manifest文件里的。如果這個地址固定的話,當游戲有新版本,需要打包測試熱更時,就必須放在這個地址下測試,而線上用戶也是這個地址,勢必會影響用戶的使用。
所以熱更的地址理想存放一個版本一個地址
game01/version1.0.0
game01/version1.0.1
game01//version2.0.0
需要修改哪里才能實現動態地址熱更?
熱更新的代碼位置,具體可以查看源碼
熱更新中manifest文件的作用如下,動態指定熱更地址需要修改游戲內resouces下manifest以及原生熱更目錄下manifest兩個地方的文件。
在HotUpdate.ts中加入修改manifest文件代碼。
當第一次熱更時,本地熱更目錄下是沒有manfiest文件的,需要創建一個並修改該文件熱更地址。
當第二次或之后熱更新,本地熱更目錄下存在manifest文件,則修改該文件熱更地址。
hotUpdateUrlCache用於判斷manifest是否已修改為最新地址,如果已修改為最新地址,則不需要再次修改。
/** * 修改.manifest文件 * @param {新的升級包地址} newAppHotUpdateUrl http://192.168.0.119:8000/remote-assets-new/ * @param {修改manifest文件后回調} resultCallback */ public modifyAppLoadUrlForManifestFile(newAppHotUpdateUrl, resultCallback) { //如果新地址和緩存地址一致,表示manifest已經被修改成最新地址,不需要再次修改manifest文件 let hotUpdateUrlCache = cc.sys.localStorage.getItem(this.hotUpdateUrlCacheKey); if (hotUpdateUrlCache && hotUpdateUrlCache == newAppHotUpdateUrl) { resultCallback(); return; } //本地熱更目錄下已存在project.manifest,則直接修改已存在的project.manifest if (jsb.fileUtils.isFileExist(this._storagePath + "/project.manifest")) { console.log("[HotUpdate] modifyAppLoadUrlForManifestFile: 有下載的manifest文件,直接修改熱更地址"); //修改project.manifest let projectManifest = jsb.fileUtils.getStringFromFile(this._storagePath + "/project.manifest"); let projectManifestObj = JSON.parse(projectManifest); projectManifestObj.packageUrl = newAppHotUpdateUrl; projectManifestObj.remoteManifestUrl = newAppHotUpdateUrl + 'project.manifest'; projectManifestObj.remoteVersionUrl = newAppHotUpdateUrl + 'version.manifest'; let afterString = JSON.stringify(projectManifestObj); let isWrittenProject = jsb.fileUtils.writeStringToFile(afterString, this._storagePath + "/project.manifest"); //更新數據庫中的新請求地址,下次如果檢測到不一致就重新修改 manifest 文件 if (isWrittenProject) { cc.sys.localStorage.setItem(this.hotUpdateUrlCacheKey, newAppHotUpdateUrl); } console.log("[HotUpdate] 修改是否成功,project.manifest:", isWrittenProject); console.log("[HotUpdate] 修改后文件:", projectManifestObj.packageUrl, projectManifestObj.remoteManifestUrl, projectManifestObj.remoteVersionUrl); resultCallback(); //不存在熱更文件夾,則新建一個熱更文件夾和project.manifest } else { console.log("[HotUpdate] modifyAppLoadUrlForManifestFile: 不存在熱更文件夾,新建一個,然后修改熱更地址"); if (!jsb.fileUtils.isDirectoryExist(this._storagePath)) jsb.fileUtils.createDirectory(this._storagePath); //修改原始project文件 let projectManifest = jsb.fileUtils.getStringFromFile(this.projectManifest.nativeUrl); let projectManifestObj = JSON.parse(projectManifest); projectManifestObj.packageUrl = newAppHotUpdateUrl; projectManifestObj.remoteManifestUrl = newAppHotUpdateUrl + 'project.manifest'; projectManifestObj.remoteVersionUrl = newAppHotUpdateUrl + 'version.manifest'; let afterString = JSON.stringify(projectManifestObj); let isWrittenProject = jsb.fileUtils.writeStringToFile(afterString, this._storagePath + '/project.manifest'); if (isWrittenProject) { cc.sys.localStorage.setItem(this.hotUpdateUrlCacheKey, newAppHotUpdateUrl); } console.log("[HotUpdate] 修改是否成功,project.manifest:", isWrittenProject); console.log("[HotUpdate] 修改后文件:", projectManifestObj.packageUrl, projectManifestObj.remoteManifestUrl, projectManifestObj.remoteVersionUrl); resultCallback(); } }
游戲開始后,先訪問后台獲取最新熱更地址,這里假設新地址是"http://192.168.0.119:8000/remote-assets/version2.0.1"。
然后調用modifyApploadUrlForManifestFile修改manifest文件的熱更地址,修改后走正常熱更流程。
this.hotUpdate.modifyAppLoadUrlForManifestFile("http://192.168.0.119:8000/remote-assets/version2.0.1", ()=>{ this.hotUpdate.checkUpdate(); });
六 強更新
打開游戲后,從后台獲取是否需要強更新,如果需要強更新,則打開瀏覽器下載新的app。
apkUrl是apk所在地址,比如http://www.test.com/test.apk
AppActivity.java:
/** * 打開瀏覽器更新下載新版本apk * @param apkUrl apk地址 */ public static void openBrowserUpdate(String apkUrl) { Intent intent = new Intent(); intent.setAction("android.intent.action.VIEW"); Uri apk_url = Uri.parse(apkUrl); intent.setData(apk_url); AppActivity.context.startActivity(intent); //startActivity無法在靜態方法中使用,所以提前保存content }
因為startActivity不能在靜態方法中使用,所以定義了一個context變量保存當前Activity。