版本: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。