https://blog.csdn.net/huanghuipost/article/details/102569135
苦海無涯,特此記錄。
先吐槽一下。官方的文檔要麼就是古董,要麼就是分散。對於我這個新上手的人來說,上來cocosCreator版本就是2.1.2了,而參考文檔還是1.X的版本、幾年前的說明。
1、準備工作
1、cocos Creator 版本 V2.1.2
2、Android 原生打包環境-->自行百度(主要是sdk,nkd,ant)
3、cocosCreator 熱更新插件 《熱更新manifest生成工具》
4、hfs網絡文件服務器 2.3(自行下載、安裝。主要用做本地簡單服務器的搭建)
2、預備知識
1、閱讀一下官方文檔
https://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html
https://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html
讀完之後,是不是就知道更新大致需要的文檔了
2、從官方文檔中給出的大致過程
3、開始熱更新
新建一個cocosCreator 項目。
1、開始還是先搭建一個簡單的服務器
1、在硬盤中創建一個目錄,用來熱更新的目錄
2、打開hfs.exe,將剛剛的目錄拖入hfs左側空白就會自動生成
這樣就簡單的生成了資源更新服務器了。
2、生成Manifest文檔和更新資源包(官方是代碼生成的,這裏我使用的cocosCreator商店的工具)
->1、創建 version_generator.js 文件 (官方代碼倉庫也有)
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//服務器上資源文件存放路徑(src,res的路徑)
packageUrl: 'http://192.168.1.120:8080/hotUpdate/',
remoteManifestUrl: 'http://192.168.1.120:8080/hotUpdate/project.manifest',
remoteVersionUrl: 'http://192.168.1.120:8080/hotUpdate/version.manifest',
version: '1.0.2',
assets: {},
searchPaths: []
};
//生成的manifest文件存放目錄
var dest = 'assets/';
//項目構建後資源的目錄
var src = 'build/jsb-default/';
/**
* node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
*/
// Parse arguments
var i = 2;
while ( i < process.argv.length) {
var arg = process.argv[i];
switch (arg) {
case '--url' :
case '-u' :
var url = process.argv[i+1];
manifest.packageUrl = url;
manifest.remoteManifestUrl = url + 'project.manifest';
manifest.remoteVersionUrl = url + 'version.manifest';
i += 2;
break;
case '--version' :
case '-v' :
manifest.version = process.argv[i+1];
i += 2;
break;
case '--src' :
case '-s' :
src = process.argv[i+1];
i += 2;
break;
case '--dest' :
case '-d' :
dest = process.argv[i+1];
i += 2;
break;
default :
i++;
break;
}
}
function readDir (dir, obj) {
var stat = fs.statSync(dir);
if (!stat.isDirectory()) {
return;
}
var subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative;
for (var i = 0; i < subpaths.length; ++i) {
if (subpaths[i][0] === '.') {
continue;
}
subpath = path.join(dir, subpaths[i]);
stat = fs.statSync(subpath);
if (stat.isDirectory()) {
readDir(subpath, obj);
}
else if (stat.isFile()) {
// Size in Bytes
size = stat['size'];
md5 = crypto.createHash('md5').update(fs.readFileSync(subpath)).digest('hex');
compressed = path.extname(subpath).toLowerCase() === '.zip';
relative = path.relative(src, subpath);
relative = relative.replace(/\\/g, '/');
relative = encodeURI(relative);
obj[relative] = {
'size' : size,
'md5' : md5
};
if (compressed) {
obj[relative].compressed = true;
}
}
}
}
var mkdirSync = function (path) {
try {
fs.mkdirSync(path);
} catch(e) {
if ( e.code != 'EEXIST' ) throw e;
}
}
// Iterate res and src folder
readDir(path.join(src, 'src'), manifest.assets);
readDir(path.join(src, 'res'), manifest.assets);
var destManifest = path.join(dest, 'project.manifest');
var destVersion = path.join(dest, 'version.manifest');
mkdirSync(dest);
fs.writeFile(destManifest, JSON.stringify(manifest), (err) => {
if (err) throw err;
console.log('Manifest successfully generated');
});
delete manifest.assets;
delete manifest.searchPaths;
fs.writeFile(destVersion, JSON.stringify(manifest), (err) => {
if (err) throw err;
console.log('Version successfully generated');
});
*****該文件主要作用是生成資源的更新信息,將創建的 version_generator.js 文檔放在主工程目錄下
-->2、打開cocosCreator熱更新工具
設置當前版本號,
第一次打包版本號可以就從1.0.0開始,
資源服務器url 對應服務器搭建的目錄我這裏是http://192.168.1.120:8080/hotUpdate/
build資源路徑就是我們在構建的時候生成的build文件模板
構建的時候,我選擇的是default,所有在熱更工具中build目錄也是這個目錄。
完成後,點擊生成,再打開目錄
已經生成版本文件。裏面就是版本更新文件,解壓後可以看到有四項文件(Manifest文件,res,src)
到這裏,需要打包用的初始Manifest文件就已經生成了。
可以將這兩個*.manifest文件拷貝到assets目錄下
接下來就是創建熱更新組件來負責這個更新邏輯了。
打開前面新建的項目,新建一個場景,一個更新邏輯js
1、創建場景,和HotUpdate.js
//HotUpdate.js
/**
* 負責熱更新邏輯的組件
*/
cc.Class({
extends: cc.Component,
properties: {
manifestUrl: cc.RawAsset, //本地project.manifest資源清單文件
_updating: false,
_canRetry: false,
_storagePath: '',
label: {
default: null,
type: cc.Label
},
},
checkCb: function (event) {
var self = this;
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
self.label.string = '本地文件丟失';
cc.log("No local manifest file found, hot update skipped.");
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log("Fail to download manifest file, hot update skipped.");
self.label.string = '下載遠程mainfest文件錯誤';
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log("Already up to date with the latest remote version.");
self.label.string = '已經是最新版本';
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
cc.log('New version found, please try to update.');
self.label.string = '有新版本發現,請點擊更新';
//this.hotUpdate(); 暫時去掉自動更新
break;
default:
return;
}
this._am.setEventCallback(null);
//this._checkListener = null;
this._updating = false;
},
updateCb: function (event) {
var self = this; // 原作者啊,你這行代碼忘加了,讓我一頓好找啊[捂臉]
var needRestart = false;
var failed = false;
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log('No local manifest file found, hot update skipped...');
self.label.string = '本地版本文件丟失,無法更新';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
cc.log(event.getPercent());
cc.log(event.getPercentByFile());
cc.log(event.getDownloadedFiles() + ' / ' + event.getTotalFiles());
cc.log(event.getDownloadedBytes() + ' / ' + event.getTotalBytes());
var msg = event.getMessage();
if (msg) {
cc.log('Updated file: ' + msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log('Fail to download manifest file, hot update skipped.');
self.label.string = '下載遠程版本文件失敗';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log('Already up to date with the latest remote version.');
self.label.string = '當前爲最新版本';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
cc.log('Update finished. ' + event.getMessage());
self.label.string = '更新完成. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log('Update failed. ' + event.getMessage());
self.label.string = '更新失敗. ' + event.getMessage();
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
self.label.string = '資源更新錯誤: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
cc.log(event.getMessage());
self.label.string = event.getMessage();
break;
default:
break;
}
if (failed) {
//cc.eventManager.removeListener(this._updateListener);
this._am.setEventCallback(null);
//this._updateListener = null;
this._updating = false;
}
if (needRestart) {
//cc.eventManager.removeListener(this._updateListener);
this._am.setEventCallback(null);
//this._updateListener = null;
// Prepend the manifest's search path
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this._am.getLocalManifest().getSearchPaths();
cc.log(JSON.stringify(newPaths));
Array.prototype.unshift(searchPaths, newPaths);
// This value will be retrieved and appended to the default search path during game startup,
// please refer to samples/js-tests/main.js for detailed usage.
// !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
cc.audioEngine.stopAll();
cc.game.restart();
}
},
retry: function () {
if (!this._updating && this._canRetry) {
this._canRetry = false;
cc.log('Retry failed Assets...');
this._am.downloadFailedAssets();
}
},
/* checkForUpdate: function () {
cc.log("start checking...");
if (this._updating) {
cc.log('Checking or updating ...');
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
cc.log(this.manifestUrl);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
cc.log('Failed to load local manifest ...');
return;
}
this._checkListener = new jsb.EventListenerAssetsManager(this._am, this.checkCb.bind(this));
this._checkListener.setEventCallback(this.checkCb.bind(this));
//cc.eventManager.addListener(this._checkListener, 1);
this._am.checkUpdate();
this._updating = true;
}, */
checkForUpdate:function(){
/* if (this._updating) {
cc.log('Checking or updating ...');
return;
} */
//cc.log("加載更新配置文件");
//this._am.loadLocalManifest(this.manifestUrl);
//cc.log(this.manifestUrl);
//this.tipLabel.string = '檢查更新';
//cc.log("start checking...");
//var state = this._am.getState()
//if (state=== jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
console.log('檢查更新')
this._am.setEventCallback(this.checkCb.bind(this));
this._failCount = 0;
this._am.checkUpdate();
this._updating = true;
// }
},
hotUpdate: function () {
if (this._am && !this._updating) {
//this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
this._am.setEventCallback(this.updateCb.bind(this));
//cc.eventManager.addListener(this._updateListener, 1);
this._am.loadLocalManifest(this.manifestUrl);
this._failCount = 0;
this._am.update();
this._updating = true;
}
},
show: function () {
// if (this.updateUI.active === false) {
// this.updateUI.active = true;
// }
},
changesence:function(){
cc.log("改變場景");
cc.director.loadScene("helloworld");
},
// use this for initialization
onLoad: function () {
var self = this;
// Hot update is only available in Native build
console.log("onloadUpdate");
if (!cc.sys.isNative) {
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remote-asset');
cc.log('Storage path for remote asset : ' + this._storagePath);
// Setup your own version compare handler, versionA and B is versions in string
// if the return value greater than 0, versionA is greater than B,
// if the return value equals 0, versionA equals to B,
// if the return value smaller than 0, versionA is smaller than B.
this.versionCompareHandle = function (versionA, versionB) {
cc.log("JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB);
self.label.string = "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;
}
};
// Init with empty manifest url for testing custom manifest
this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
// Setup the verification callback, but we don't have md5 check function yet, so only print some message
// Return true if the verification passed, otherwise return false
this._am.setVerifyCallback(function (path, asset) {
// When asset is compressed, we don't need to check its md5, because zip file have been deleted.
var compressed = asset.compressed;
// Retrieve the correct md5 value.
var expectedMD5 = asset.md5;
// asset.path is relative path and path is absolute.
var relativePath = asset.path;
// The size of asset file, but this value could be absent.
var size = asset.size;
if (compressed) {
cc.log("Verification passed : " + relativePath);
return true;
}
else {
cc.log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
return true;
}
}.bind(this));
cc.log("Hot update is ready, please check or directly update.");
if (cc.sys.os === cc.sys.OS_ANDROID) {
// Some Android device may slow down the download process when concurrent tasks is too much.
// The value may not be accurate, please do more test and find what's most suitable for your game.
this._am.setMaxConcurrentTask(2);
cc.log("Max concurrent tasks count have been limited to 2");
}
this._am.loadLocalManifest(this.manifestUrl);
cc.log(this.manifestUrl);
//檢查更新
this.checkUpdate()
},
checkUpdate:function() {
console.log('檢查更新')
this._am.setEventCallback(this.checkCb.bind(this));
this._failCount = 0;
this._am.checkUpdate();
this._updating = true;
},
onDestroy: function () {
if (this._updateListener) {
//cc.eventManager.removeListener(this._updateListener);
this._am.setEventCallback(null);
//this._updateListener = null;
}
//if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
// this._am.release();
//}
}
});
新建熱更新場景,綁定腳本,創建元素三個按鈕,一個標籤。
將HotUpdate.js綁定到canvas上,將對應的節點添加到上去。
在場景文件中,創建三個按鈕,一個標籤。並分別綁定按鈕事件。
其中切換場景就是查看熱更新效果。
這個project就是我們在使用熱更新工具生成出來的manifest文件
第二步完成
接下來,再一次構建項目。注意順序
在目錄build\jsb-default中找到main.js的開頭處添加代碼
if (jsb) {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
博主提醒:上邊這段代碼每次構建的時候會特麼的消失,你得自己重新再加一遍,我在這被坑了很久[捂臉]
這裏你可能會疑惑,這個HotUpdateSearchPaths是什麼,它是HotUpdate.js中updateCb函數中設置的對應的目錄。
這個時候回到剛剛編譯的界面,記得不要再構建了,直接編譯出包。
等待中。。。
出包後,放到手機上安裝。這個包就是當做舊版本包來使用。前面設置的版本是1.0.0
其實到這裏,熱更新已經完成了。測試一下。。注意順序
在項目中,修改helloWord場景的元素。然後構建(這個時候,不是新包,不需要修改main.js 中的內容),然後打開熱更新工具,修改版本號(提高一個版本),生成。將生成的資源拷貝到搭建的服務器熱更目錄下,解壓。
這個時候再打開手機上的apk,就會發現提示新版本下載。
熱更到此結束(羅裏吧嗦的一大堆)!
如果還是沒有成功。那就再檢查一下
1、在version_generator.js文件中
2、在構建好的目錄中
在該目錄下的main.js中沒有添加更新後的路徑搜索代碼
要還是崩。。。那就自己先看日誌自己解決吧(其他的日誌都在手機上會顯示)。
最後提一下
以前很多前輩寫的一寫文章都是摸爬打滾出來的。很有參考價值。
但是,奈何敵不過CocosCreator自己的版本更新。
所以很多的bug都是沒有試用新版本而已。及時關注他們的更新指南。
這是2.0的升級指南:https://docs.cocos.com/creator/manual/zh/release-notes/upgrade-guide-v2.0.html
裏面就改進了監聽(主要是有個問題困擾我很久,最後看網友說去看升級指南。。。,然後就看到問題所在了。)
最後感謝這位博主https://www.cnblogs.com/gao88/p/11632626.html,給出了一個非常詳細的過程。
最後指出一下,本博文只是簡單的熱更新。在代碼中有很多屏蔽的地方,都是奔潰的。我只是讓它實現了最基本的熱更新。
還沒有測試ios下,也沒有太多的優化。比如進度條顯示、文件顯示。
後續再更新吧!
今天先到這裏。
--------------------------------------------------------------------------------------------------------
https://blog.csdn.net/qq_40956352/article/details/90442679
Cocos Creator 熱更新(動態修改熱更地址)
入門CocosCreator大概一年,由於項目需要,要用到熱更新,由於之前沒接觸過,於是根據官方文檔把熱更新走了一遍後,其中遇到各種問題,大大小小的坑也爬了不少,於是把它記錄下來也方便自己以後查看,希望對需要的人也有點幫助。
環境準備
1.搭建好cocos打包環境(可自行百度)
2.熱更環境(這裏使用的是cocos官方商店插件,自己可以去商店下載安裝即可,安裝完成後重啓Cocos,這裏也不做過多介紹)
3. 下載官方熱更範列教程
接下來進入正題。
熱更流程
一.我們先打開官方教程,對照官方文檔,瞭解一下大致流程,發現熱更流程大致分爲下面四點:
1.基於原生打包目錄中的 res 和 src 目錄生成本地 Manifest 文件。
2.創建一個熱更新組件來負責熱更新邏輯。
3.遊戲發佈後,若需要更新版本,則生成一套遠程版本資源,包含 res 目錄、src 目錄和 Manifest 文件,將遠程版本部署到服務端。
4.當熱更新組件檢測到服務端 Manifest 版本不一致時,就會開始熱更新
具體請參考官方地址 熱更新官方案列
下面詳細介紹更新步驟
1.先構建
2.在項目下面打開熱更新工具(前面在商店下載安裝的熱更新工具)
填寫好對應的信息,點擊生成即可。
3.如果第一次出包,則找到剛生成的熱更新資源(在工程文件根目錄下會生成packVersion,剛生成的資源包就在這個文件夾下),解壓,然後發現四個文件,sre和res主要是資源文件和代碼,project.manifest和version.mainfast
將project.manifest和version.manifest拷貝到工程目錄下覆蓋之前的(官方的mainfast直接寫在了hotupdate.js裏面,我們當然不能這樣做,於是把他放在我的工程目錄下),我目前工程放在這裏,然後在重新構建。
4.在build文件夾下找到main.js文件
打開main.Js,在開頭加上紅色框裏面的代碼(不加的話會導致熱更完下次打開遊戲還是之前的資源)這裏幫你寫好了,直接複製即可(這裏有一個問題注意,官方if (cc.sys.isNative) {}我按照這樣寫,android啓動遊戲會黑屏,然後改成if (jsb) {})
if (jsb) {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
這裏是hotupdate.js代碼
cc.Class({
extends: cc.Component,
properties: {
panel: UpdatePanel,
manifestUrl: {
type: cc.Asset,
default: null
},
updateUI: cc.Node,
_updating: false,
_canRetry: false,
_storagePath: ''
},
checkCb: function (event) {
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode())
{
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.panel.info.string = "No local manifest file found, hot update skipped.";
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.panel.info.string = "Fail to download manifest file, hot update skipped.";
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.panel.info.string = "Already up to date with the latest remote version.";
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.panel.info.string = 'New version found, please try to update.';
this.panel.checkBtn.active = false;
this.panel.fileProgress.progress = 0;
this.panel.byteProgress.progress = 0;
break;
default:
return;
}
this._am.setEventCallback(null);
this._checkListener = null;
this._updating = false;
},
updateCb: function (event) {
var needRestart = false;
var failed = false;
switch (event.getEventCode())
{
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.panel.info.string = 'No local manifest file found, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
this.panel.byteProgress.progress = event.getPercent();
this.panel.fileProgress.progress = event.getPercentByFile();
this.panel.fileLabel.string = event.getDownloadedFiles() + ' / ' + event.getTotalFiles();
this.panel.byteLabel.string = event.getDownloadedBytes() + ' / ' + event.getTotalBytes();
var msg = event.getMessage();
if (msg) {
this.panel.info.string = 'Updated file: ' + msg;
// cc.log(event.getPercent()/100 + '% : ' + msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.panel.info.string = 'Fail to download manifest file, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.panel.info.string = 'Already up to date with the latest remote version.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.panel.info.string = 'Update finished. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.panel.info.string = 'Update failed. ' + event.getMessage();
this.panel.retryBtn.active = true;
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.panel.info.string = 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.panel.info.string = event.getMessage();
break;
default:
break;
}
if (failed) {
this._am.setEventCallback(null);
this._updateListener = null;
this._updating = false;
}
if (needRestart) {
this._am.setEventCallback(null);
this._updateListener = null;
// Prepend the manifest's search path
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this._am.getLocalManifest().getSearchPaths();
console.log(JSON.stringify(newPaths));
Array.prototype.unshift.apply(searchPaths, newPaths);
// This value will be retrieved and appended to the default search path during game startup,
// please refer to samples/js-tests/main.js for detailed usage.
// !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
cc.audioEngine.stopAll();
cc.game.restart();
}
},
loadCustomManifest: function () {
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
var manifest = new jsb.Manifest(customManifestStr, this._storagePath);
this._am.loadLocalManifest(manifest, this._storagePath);
this.panel.info.string = 'Using custom manifest';
}
},
retry: function () {
if (!this._updating && this._canRetry) {
this.panel.retryBtn.active = false;
this._canRetry = false;
this.panel.info.string = 'Retry failed Assets...';
this._am.downloadFailedAssets();
}
},
checkUpdate: function () {
if (this._updating) {
this.panel.info.string = 'Checking or updating ...';
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
var url = this.manifestUrl.nativeUrl;
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this._am.loadLocalManifest(url);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
this.panel.info.string = 'Failed to load local manifest ...';
return;
}
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
this._updating = true;
},
hotUpdate: function () {
if (this._am && !this._updating) {
this._am.setEventCallback(this.updateCb.bind(this));
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
var url = this.manifestUrl.nativeUrl;
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this._am.loadLocalManifest(url);
}
this._failCount = 0;
this._am.update();
this.panel.updateBtn.active = false;
this._updating = true;
}
},
show: function () {
if (this.updateUI.active === false) {
this.updateUI.active = true;
}
},
// use this for initialization
onLoad: function () {
// Hot update is only available in Native build
if (!cc.sys.isNative) {
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'blackjack-remote-asset');
cc.log('Storage path for remote asset : ' + this._storagePath);
// Setup your own version compare handler, versionA and B is versions in string
// if the return value greater than 0, versionA is greater than B,
// if the return value equals 0, versionA equals to B,
// if the return value smaller than 0, versionA is smaller than B.
this.versionCompareHandle = function (versionA, versionB) {
cc.log("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;
}
};
// Init with empty manifest url for testing custom manifest
this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
var panel = this.panel;
// Setup the verification callback, but we don't have md5 check function yet, so only print some message
// Return true if the verification passed, otherwise return false
this._am.setVerifyCallback(function (path, asset) {
// When asset is compressed, we don't need to check its md5, because zip file have been deleted.
var compressed = asset.compressed;
// Retrieve the correct md5 value.
var expectedMD5 = asset.md5;
// asset.path is relative path and path is absolute.
var relativePath = asset.path;
// The size of asset file, but this value could be absent.
var size = asset.size;
if (compressed) {
panel.info.string = "Verification passed : " + relativePath;
return true;
}
else {
panel.info.string = "Verification passed : " + relativePath + ' (' + expectedMD5 + ')';
return true;
}
});
this.panel.info.string = 'Hot update is ready, please check or directly update.';
if (cc.sys.os === cc.sys.OS_ANDROID) {
// Some Android device may slow down the download process when concurrent tasks is too much.
// The value may not be accurate, please do more test and find what's most suitable for your game.
this._am.setMaxConcurrentTask(2);
this.panel.info.string = "Max concurrent tasks count have been limited to 2";
}
this.panel.fileProgress.progress = 0;
this.panel.byteProgress.progress = 0;
},
onDestroy: function () {
if (this._updateListener) {
this._am.setEventCallback(null);
this._updateListener = null;
}
}
});
爬坑
當然按照這個流程熱更新是沒有問題得,但是我們當時有一個需求是需要根據服務器傳過來得地址去動態選擇熱更新地址,那這個需求就不能滿足我們了,於是又去各種查資料,發現只要能動態改變project.mainfast裏面得地址就可以達到這個效果。於是就有兩種情況。
① 用戶從未進行過熱更新,這個時候App內的.manifest文件只有一份,我們只需要修改這個.manifest文件即可。
②用戶在安裝該App期間使用過熱更新,這時候App內部就會存在兩份project.manifest文件了,此時我們要修改的project.manifest、version.manifest文件位於當初我們在項目中指定的熱更新目錄位置。如我的項目是:
這個目錄根據自己寫的實際情況來,然後就是修改project.mainfast的值(這裏copy一下別人的代碼)
modifyAppLoadUrlForManifestFile(newAppHotUpdateUrl, localManifestPath, resultCallback) {
if (jsb.fileUtils.isFileExist(jsb.fileUtils.getWritablePath() + 'remoteAssets/project.manifest')) {
console.log("有下載的manifest文件");
let storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remoteAssets');
console.log("StoragePath for remote asset : ", storagePath);
let loadManifest = jsb.fileUtils.getStringFromFile(storagePath + '/project.manifest');
let manifestObject = JSON.parse(loadManifest);
manifestObject.packageUrl = "遠程服務器地址';
manifestObject.remoteManifestUrl = "遠程服務器地址" + 'project.manifest';
manifestObject.remoteVersionUrl = "遠程服務器地址" + 'version.manifest';
let afterString = JSON.stringify(manifestObject);
let isWritten = jsb.fileUtils.writeStringToFile(afterString, storagePath + '/project.manifest');
let initializedManifestPath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remoteAssets');
if (!jsb.fileUtils.isDirectoryExist(initializedManifestPath)) jsb.fileUtils.createDirectory(initializedManifestPath);
console.log("storagePath==", initializedManifestPath);
console.log("沒有下載的manifest文件", newAppHotUpdateUrl);
//修改原始manifest文件
let originManifestPath = localManifestPath;
let originManifest = jsb.fileUtils.getStringFromFile(originManifestPath);
let originManifestObject = JSON.parse(originManifest);
originManifestObject.packageUrl = newAppHotUpdateUrl;
originManifestObject.remoteManifestUrl = originManifestObject.packageUrl + 'project.manifest';
originManifestObject.remoteVersionUrl = originManifestObject.packageUrl + 'version.manifest';
let afterString = JSON.stringify(originManifestObject);
let isWritten = jsb.fileUtils.writeStringToFile(afterString, initializedManifestPath + '/project.manifest');
console.log("Written Status : ", isWritten);
}
},
修改完之後,在把這個project.mainfast轉成mainfast文件,接下來就可以根據呢服務器傳過來的地址動態修改熱更地址了。
還有一個問題就是更新下載的資源是根據你遠程project.mainfast的地址下載的。本地的project.mainfast主要用於下載遠程project.mainfast,然後和遠程比對MD5的值,不同的就下載。
到這裏熱更基本上流程已經走完了。
但是最後我們又遇到一個問題,就是熱更過後,設置優先搜索路徑之後(前面在main.js中加入的代碼)會把你的熱更路徑設爲優先搜索路徑,下次遇到大版本更新的話,還是會優先從這個路徑進行搜索,然後就會出現問題。所以,當遇到大版本更新的的時候,我們需要清除自己熱更路徑下資源。想要徹底清理一次本地的熱更新緩存,那麼怎麼做呢,後來發現可以記錄當前的遊戲版本,檢查 cc.sys.localStorage 中保存的版本是否匹配,如果不匹配則可以做一次清理操作:
// 之前版本保存在 local Storage 中的版本號,如果沒有認爲是舊版本
var previousVersion = parseFloat( cc.sys.localStorage.getItem('currentVersion') );
// game.currentVersion 爲該版本的常量
if (previousVersion < game.currentVersion) {
// 熱更新的儲存路徑,如果舊版本中有多個,可能需要記錄在列表中,全部清理
jsb.fileUtils.removeDirectory(storagePath);
}
好了,到這裏基本上就結束了。(第一次發博客,如果發現有什麼說的不對的地方,歡迎指正)
更多參考: