热更新的原理
客户端存在一个project.manifest文件,该文件包含几个信息:
- packageUrl:url,服务器更新数据包根目录;
- remoteManifestUrl:url, [可选项]服务器上project.manifest文件的url,
- remoteVersionUrl:url,服务器上version.mainifest文件的url;
- version:x.x.x,项目版本;
- assets:{},资源列表;
key : 资源的相对路径(相对于资源根目录)
md5 : md5 值代表资源文件的版本信息
compressed: [可选项] 如果值为 true,文件被下载后会自动被解压,目前仅支持 zip 压缩格式
size: [可选项] 文件的字节尺寸,用于快速获取进度信息 - searchPaths:" ",搜索路径
客户端通过本地的project.manifest中url,可以获取服务器上project.manifest文件,比较两者的version属性,如果客户端的version比服务器低,则启动更新。
更新的内容:assets是文件列表,里面列出了项目中的完整资源,每个资源都有md5表示,客户端根据本地project.manifest中的assets列表和服务器的assets列表对比,下载不同的资源到临时文件夹,如果最后所有资源都正常,则把临时文件夹的内容替换到本地缓存文件夹中,并且修改优先搜索路径为该文件夹。所以重启游戏之后的使用的资源优先从缓存文件夹中搜索。
需要环境
- nodejs
- cocoscreator
官方案例
-
下载官方范例,解压。该案例已经把客户端和服务器的资源都打包好了,更新包在remote-assets文件夹中。
image.png
-
服务器--我使用的是nodejs。
1 .新建一个文件夹nodejs,在nodejs中新建hotUpdate文件夹,在把官方案例中的remote-assets复制到hotUpdate文件夹中。
image.png
2 .在nodejs中新建一个js脚本,脚本内容如下
var express = require('express');
var path = require('path');
var app = express();
app.use(express.static(path.join(__dirname, 'hotUpdate')));
app.listen(80);
3.在nodejs文件夹下执行node app.js命令,启动服务器,可以访问http://127.0.0.1/remote-assets/project.manifest,如果成功访问则服务器启动成功。
image.png
接下来修改manifest文件里面的url,有三个文件需要修改,服务器remote-assets中的两个manifest后缀文件,官方项目assets文件夹下的project.manifest
image.png
image.png
只修改三个url
image.png
修改完成之后,打开项目,用模拟器运行看看效果。
点击检查更新按钮
image.png
点击立即更新按钮
image.png
因为某些原因,模拟器更新完成,重启不能使用更新的资源,所以需要编译成原生,我的电脑不能编译windows,就不上图了。打包成apk自测可以成功更新。
实际项目实现:
1:旧版本制作:
添加场景或者保持原来的场景和代码结构不变,打包到原生平台,记住打包完之后将main.js的代码加上:
if (cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
然后再重新编译将编译出来的应用安装到原生平台上我这里安装到了我的手机上。现在手机上的应用就有了热更新的代码和自动检测搜索路径的代码,一旦本地版本和远程版本不一致就会更新localStorage里面的键值,并且手机端就会更新搜索路径加载新加的资源文件。
检测更新和立即更新代码:
const {ccclass, property} = cc._decorator;
@ccclass
export default class NewClass extends cc.Component {
@property({
type: cc.ProgressBar
})
byteProgress: cc.ProgressBar;
@property({
type: cc.ProgressBar
})
fileProgress: cc.ProgressBar;
@property({
type: cc.Label
})
fileLabel: cc.Label;
@property({
type: cc.Label
})
byteLabel: cc.Label;
@property({
type: cc.Asset
})
manifesetUrl: cc.Asset = null;
@property({
type: cc.Label
})
oupPut: cc.Label = null;
private fileRate: number = 0;
private byteRate: number = 0;
private am: any;
private storagePath: string;
onLoad () {
if(!cc.sys.isNative) {
return;
}
}
start () {
if(cc.sys.isNative) {
console.log("is native plateform");
try {
console.log("Jsb is ",jsb);
this.storagePath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "/") + "blackjace-remote-asset";
console.log("storegePaht is ",this.storagePath);
this.am = new jsb.AssetsManager("",this.storagePath);
console.log("am is ",this.am);
// this.am.setVerifyCallback((path,asset) => {
// console.log("path is ",path);
// console.log("asset is ",asset);
// })
} catch(e) {
console.log("e is ",e);
}
}
}
// 检查更新
checkUpdate(): void {
// 原生平台
if(CC_JSB) {
this.oupPut.string = "enter native plateform";
this.oupPut.string = "am's state is " + this.am.getState();
console.log("state is ",this.am.getState());
console.log("jsb's state is ",jsb.AssetsManager.State.UNINITED);
if(this.am.getState() === jsb.AssetsManager.State.UNINITED) {
let url = this.manifesetUrl.nativeUrl;
console.log("Url is ",url);
this.oupPut.string = "url is " + url;
console.log("loader's md5Pipe is ",cc.loader.md5Pipe);
if(cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
console.log("after transfrom url is ",url);
}
this.oupPut.string = "url is " + url;
this.am.loadLocalManifest(url);
if(!this.am.getLocalManifest() || !this.am.getLocalManifest().isLoaded()) {
this.oupPut.string = "can not loaded manifest file";
return;
}
this.oupPut.string = "checking....";
this.am.setEventCallback(this.checkCallback.bind(this));
this.am.checkUpdate();
} else {
this.oupPut.string = "up to date";
}
}
}
// 检查更新的回调函数
private checkCallback(event: any): void {
let self = this;
console.log("code is ",event.getEventCode());
this.oupPut.string = "===>>>> checking over";
switch(event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.oupPut.string = "No local manifest file found,hot update skipped";
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.oupPut.string = "Fail to download manifest file";
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.oupPut.string = "Alread up to date with the latest remote version";
let timeId = setTimeout(() => {
clearTimeout(timeId);
// 加载新的场景
self.node.active = false;
cc.director.loadScene("newest");
},2000);
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.oupPut.string = "NEW version found,please try to update";
break;
default:
return;
}
this.am.setEventCallback(null);
}
// 立即更新
updateNow(): void {
if(this.am) {
// 绑定事件
this.am.setEventCallback(this.updateCallback.bind(this));
if(this.am.getState() === jsb.AssetsManager.State.UNINITED) {
let url = this.manifesetUrl.nativeUrl;
if(cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this.am.loadLocalManifest(url);
}
this.am.update();
}
}
// 更新回调函数
private updateCallback(event: any): void {
let needRestart: boolean = false;
switch(event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.oupPut.string = "no local manifest file found";
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
this.fileProgress.progress = event.getPercentByFile();
this.fileLabel.string = event.getDownloadedFiles() + " / " + event.getTotalFiles();
let rate = event.getDownloadedFiles() / event.getTotalFiles();
if(rate >= 1) {
this.node.active = false;
cc.director.loadScene("newest");
}
let message = event.getMessage();
if(message) {
this.oupPut.string = "Update file: " + message;
}
break;
case jsb.EventAssetsManager.ERROR.DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.oupPut.string = "fail to download manifest file";
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.oupPut.string = "already up to date";
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.oupPut.string = "update finished";
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.oupPut.string = "update file failed";
needRestart = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.oupPut.string = "on updating occure error";
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.oupPut.string = event.getMessage();
break;
default:
break;
}
// 需要重新启动
if(needRestart) {
this.am.setEventCallback(null);
// 得到manifest的搜索路径
let searchPaths = jsb.fileUtils.getSearchPaths();
console.log("searchPath si ",searchPaths);
let newPaths = this.am.getLocalManifest().getSearchPaths();
console.log(JSON.stringify(newPaths));
Array.prototype.unshift.apply(searchPaths,newPaths);
console.log("searchPaths is ",searchPaths);
console.log("newPaths is ",newPaths);
cc.sys.localStorage.setItem("HotUpdateSearchPaths",JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
// 重启游戏
cc.game.restart();
}
}
update (dt) {
}
}
2:制作新版本:新版本的游戏制作完成之后构建编译,修改main.js文件同上重新编译
运行version_generator.js文件这里我编写了bat文件:
node version_generator.js -v 1.3.5 -u http://172.16.2.46:8080/remote-assets/ -s build/jsb-link/ -d assets/
到时候需要变更版本直接改变-v后面的参数,将res,src文件,version.manifest,project.manifest文件复制到node.js文件配置的静态文件访问目录就行了
pm2重启服务器,在在前的手机上打开应用,开始更新