最近一段时间在整理热更新,顺便做一些总结。creator没有自带热更新,有带示例,但都没有很好的解决问题。
1.引擎热更新部分没有考虑远程包的热更新,不想把所有包当成正常包来更新下载,这样一开始要检查特别多的文件,特别是项目很大的时候。希望是游戏一进入更新检查主包,需要进入子游戏(设置为远程包)部分,游戏再检查下载远程包更新。
2.引擎代码热更完之后都得重新启动(虽然是热重启),首次进入检查更新时重启还可以接收,但当游戏进入一半更新子游戏时还重启就不够好。
3.自带远程bundle版本更新,Asset Bundle 在更新上延续了 Creator 的 MD5 方案: assetManager.loadBundle(远程url+bundleName, {version: 'fbc07'}, function (err, bundle) {
if (err) return console.error(err); console.log('load bundle successfully.');
});
这种方式更新,是更据远程bundle下config.json下读取文件名,需要时下载远程url加文件名下载文件后,缓存在 writablePath/files/gamecache/下,然后所有文件重新以时间命名,并添加一个url与新文件之间的映射关系文件gamecahe.txt。所以,更新只能用md5方式,每次以新文件方式更新,就产生许多的旧文件,然后多一层映射关系文件gamecahe.txt,同时也不会删除旧文件映射关系只会越来越大,也降低了性能。
loadBundle 首次只加载config.json和index.js,并会判断index.js是否执行过,未执行就会执行,所有md5方式会重新执行,而非md5不执行(因为引擎从来都是考虑代码更新后必须重启的方式)。
因为上面3个问题,所以想自己实现一个更完美的热更新方案:首次进入加载首包,如果有更新下载,热重启应用进入游戏。进入游戏后再进入子游戏时,检查并更新下载子游戏(远程包),如果有新的更新,删除旧包数据及代码,直接进入子游戏,并不重启APP。。下面是实现在细节:
1.添加cocoscreator 插件,主要实现以下功能:
(1)在build完成后onAfterBuild(),在main.js中注入代码(参考官方示例),从热更新目录里优化查找资源(比包内资源优化),从而能使用最新资源,那如果要更新main.js呢??因为main.js里的代码是固定的不必更新,如果非为更新,只能再c++代码里添加热更新目录而不是main.js中添加了。。
(2)为主包以及所有的远程包(remote文件夹里所有的文件夹)生成version.manifes和project.manifest,基中version.manifest只有在服务端需要,请求是否有新版本的时候(只要版本号不一样就更新,从而支持版本回滚),并不需要完整的project.manifest,从而节省流量并加快速度。
2.项目添加热更新管理代码(hotTool单例)
为什么不像示例一样做成组件?官方示例只有一个地方更新,所以做成组件也只有一个地方用,而我们是要在大厅加载所有子游戏,给每个子游戏添加热更新组件不仅麻烦,效率也不高。更新时只有传入以下参数就可以实现,主包和所有子游戏更新
export type HotupdateParam = {
subgame?: string, // bundle名 用于子游戏热更时,则一定要传
onProgress?: (event: native.EventAssetsManager) => void, // 热更过程
onComplete: (findNew: boolean) => void, // 热更完成
onFail: (event: native.EventAssetsManager) => void, // 热更失败
onStart?: () => void // 发现新版本,则开始热更新
}
3. 热更新代码生效
主包只要热重启即可,因为刚启动游戏就检查主包,有更新的话再启动也是合理的,
主包更新完成进入主界面后,检查子游戏更新,子游戏要删除所有包内的脚本和包内cclass,再把bundler所有资源释放干净后,然后就可以直接进入子游戏了。
/** bundle 脚本表 */
const bundleScriptTab: Record<string, any> = {};
/** js 系统 */
const systemJs = window["System"];
/** 脚本缓存表 */
const scriptCacheTab: Record<string, any> = systemJs[Reflect.ownKeys(systemJs).find((v) => typeof v === "symbol")!];
// 初始化 bundle 脚本表
Object.keys(scriptCacheTab).forEach((vs) => {
const current = scriptCacheTab[vs] as { d: any[]; id: string };
const parent = scriptCacheTab[vs].p;
if (!parent?.d || current.id !== parent.id) {
return;
}
const names = parent.id.slice((parent.id as string).lastIndexOf("/") + 1);
bundleScriptTab[names] = parent;
});
// 清理脚本缓存
{
const bundleRoot = bundleScriptTab[bundleName]?.d[0];
if (bundleRoot) {
bundleRoot.d.forEach((v: { id: string }) => {
systemJs.delete(v.id);
});
systemJs.delete(bundleRoot.id);
systemJs.delete(bundleRoot.p.id);
}
}
// 清理 ccclass
{
const reg = new RegExp(`${bundleName}-`);
Object.keys(js._nameToClass)
.filter((vs) => vs.match(reg) !== null)
.forEach((vs2) => {
js.unregisterClass(js.getClassByName(vs2));
});
}
// 清理 bundle 资源
{
const bundle = assetManager.getBundle(bundleName)
if (bundle) {
bundle.releaseAll();
assetManager.removeBundle(bundle);
}
}
cclass要 以/区分,这样也支持编辑器启动,其它标志也可以,但是编辑器启动会出问题