前面 学了 android cocoscreator 相互调用(三)
这章 主要学习在 android 里 热更 js 代码及资源(cocoscreator 官方也有介绍,不过不是很详细,也有用热插件的,商店里有),这章介绍手动操作脚本生成热更新文件及测试热更新
1: 准备
win7 64位
cocoscreator2.0.10 (新版本如 2.4.7应该也是可以的)
node 版本 13.14 版 (再高好像不支持win7了)
2:创建个helloworld 工程 再生成APK
1:增加2个按钮 check hotupdate 分别绑定 checkfun hotupdatefun 函数
cc.Class({
extends: cc.Component,
properties: {
label: {
default: null,
type: cc.Label
},
// defaults, set visually when attaching this script to the Canvas
// text: 'Hello, World!',
/eh///
//包名 暂定 org.cocos2d.helloworld
//project.mainfest 文件存放地
manifestUrl: {
type: cc.Asset, // use 'type:' to define Asset object directly
default: null, // object's default value is null
},
_updating: false, //是否正在更新中
_canRetry: false, //能否重试
_storagePath: '' //android /data/data/包名/files/ 下就是这个存放目录
/
},
// use this for initialization
onLoad: function () {
// this.label.string = this.text;
/
this.outputlog("onLoad");
// Hot update is only available in Native build
if (!cc.sys.isNative) {
this.outputlog("jsb is not isNative");
return;
}
if(!jsb){
this.outputlog("jsb is null");
return ;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'my-remote-asset');// /data/data/包名/files/my-remote-asset
this.outputlog('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.
var self = this;
this.versionCompareHandle = function (versionA, versionB) {
self .outputlog("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);
// if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) { // this._am.retain(); 没有这个函数 eh
// this._am.retain();
// }
// 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) {
self .outputlog("Verification passed : " + relativePath);
return true;
}
else {
self .outputlog("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
return true;
}
});
this.outputlog("Hot update is ready, please check or directly update.");
this.label.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.outputlog("Max concurrent tasks count have been limited to 2");
}
// this.checkfun();
///
},
// called every frame
update: function (dt) {
},
/eh///
//日志输出
outputlog:function(){//可变参数 arguments
let strlog="";
for (var i=0; i<arguments.length; i++) {
strlog = strlog + arguments[i];
}
console.log(strlog);
},
showlablestr:function(str){
this.label.string = str ;
},
button 绑定函数
checkfun:function(){
if (this._updating) {
this.label.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.label.string = 'Failed to load local manifest ...';
return;
}
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
this._updating = true;
},
button 绑定函数
hotupdatefun: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._am.update();
this._updating = true;
}
},
/热更代码 参考官方///
checkCb: function (event) {
this.outputlog('Code: ' + event.getEventCode());
switch (event.getEventCode())
{
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.showlablestr("No local manifest file found, hot update skipped.");
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.showlablestr("Fail to download manifest file, hot update skipped.");
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.showlablestr( "Already up to date with the latest remote version.");
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.showlablestr('New version found, please try to update.');
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.showlablestr( 'No local manifest file found, hot update skipped.');
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.showlablestr('Fail to download manifest file, hot update skipped.');
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.showlablestr('Already up to date with the latest remote version.');
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.showlablestr( 'Update finished. ' + event.getMessage());
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.showlablestr( 'Update failed. ' + event.getMessage());
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.showlablestr( 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.showlablestr( 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();
this.outputlog(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.
//eh explain 跟main.js var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
//必须一致 必须一致 必须一致
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.showlablestr( 'Using custom manifest');
}
},
retry: function () {
if (!this._updating && this._canRetry) {
this._canRetry = false;
this.showlablestr( 'Retry failed Assets...');
this._am.downloadFailedAssets();
}
},
onDestroy: function () {
if (this._updateListener) {
this._am.setEventCallback(null);
this._updateListener = null;
}
},
/
});
3: 构建再执行 脚本生成 manifest 文件
1> 先构建**(第一次)**
构建是为了生成 build\jsb-link(或jsb_default)\ 这里选择的是link 模板
主要就是为了res 和 src 这两个目录 为后面生成manifest文件用
2> 把 version_generator.js 放到工程根目录下
version_generator.js内容如下
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//记得先装个node 啊
//packageUrl 服务器更新数据包根目录 hotupdate 目录下有 src res 加 2个 manifest(project,version) 如下
//version 版本号
packageUrl: 'http://192.168.1.4/hotupdate/',
remoteManifestUrl: 'http://192.168.1.4/hotupdate/project.manifest',
remoteVersionUrl: 'http://192.168.1.4/hotupdate/version.manifest',
version: '1.0.0',
assets: {},
searchPaths: []
};
//生成的manifest文件存放目录 就是用node version_generator.js 生成2个manifest 存放的路径
var dest = 'assets/';
//项目构建后资源的目录
var src = 'build/jsb-link/'; //这里选择的是link模板,选择default 需要修改下
// 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, 'binary')).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');
});
这里已经把远程路径配好了,所以只需要 执行 node version_generator.js 会产生manifest 文件
这里第一次版本号先用1.0.0 (下次为个 1.0.1 或下次版本自己随意增加)
如果没配置或跟文件里的不一样可以用 命令
node version_generator.js -v 1.0.0 -u http://192.168.1.4/hotupdate/ -s build/jsb-link/ -d assets/
就4个附加参数 -v -u -s -d 知道为什么? 跟 version_generator.js 一一对应的
执行 node version_generator.js 或 node version_generator.js -v -u -s -d 带参数这种(个人觉得直接在文件里写好省事)
会在assets目录下生成2个manifest文件,为什么会生成在这,看 version_generator.js 里配置 或看 -d 的参数
把project.manifest 文件 拖过去
保存场景 再次构建**(第二次)** ,
构建完了后,修改build\jsb-link\main.js 在 window.boot(); 前 (因为选择的是link 模板)
var isRuntime = (typeof loadRuntime === 'function');
if (isRuntime) {
require('src/settings.js');
require('src/cocos2d-runtime.js');
require('jsb-adapter/engine/index.js');
}
else {
require('src/settings.js');
require('src/cocos2d-jsb.js');
require('jsb-adapter/jsb-engine.js');
}
///eh add/
//HelloWorld.js updateCb 函数 cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths'); //必须一致
if (hotUpdateSearchPaths) {
window.jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
require('src/settings.js'); //重新加载一次 或上面的就不加载
cc.macro.CLEANUP_IMAGE_CACHE = true;
window.boot();
保存main.js,再点编译生成APK (根据机子好坏等段时间)
编译完成得到APK,路径 这里选择的是link模板,选择default 路径有点区别的
以上是生成APK的工程,可以保存起来,以后修改都是在这个基础上的进行的
3:修改工程测试热跟新
1: 第一次测试,先修改 icon 再加个新的 lable ,简单修改下代码
目标:热更新完了,左下角新增加了lable 显示 原lable内容+[测试] 2个字
cocos 图标 会增加一条竖的红线
修改完后保存,构建就可以了,构建完了,修改 version_generator.js里的 版本号 这里修改为1.0.1
修改完后保存, 再执行 node version_generator.js 生成新的 2个manifest ,
把生成的 project.manifest version.manifest(这里在assets 目录下,不确定是最新的话,看修改时日期)
及 build\jsb-link 目录下 res,src 这两个目录一起放到 packageUrl 表示的服务器目录下,
这里是本地测试,192.168.1.4是本机IP,装的是xampp ,apk 装在模拟器里测试的
安装完APK 的显示
点击check
点击hotupdate 结果如下图 ,重新启动APK,结果也如下图,这样代表 热更新OK了
最后看看在android 里的热更新 下载代码与资源存放目录
运行在夜神模拟器7.0.0.5里,路径 /data/data/包名/files 下
4 热更新及测试结束(记得保存一份生成APK的工程代码,后面任何改动可以以此为基础)
1: project.manifest 一定要在 manifestUrl:{ type: cc.Asset,… } 上,不需要删,执行 node version_generator.js 会覆盖的
2: 只需要构建
3: 修改版本号 (version_generator.js 里)
4: 执行 node version_generator.js
5: 把相应文件及目录放入远端服务器目录下就行了
工程代码如有需要后续再上传