一直想写一个关于游戏更新的教程,自己的对更新的理解,以及项目中遇到的问题和解决方案分享给大家。希望兄弟们遇到其他问题的来补充一下。共同交流进步。
大部分问题都能通过热更新解决,但是一些需要修改原生代码的功能,和引擎相关的修复,只能通过整包更新完成。所以整包更新在所难免。
整包更新流程
1.获取app版本信息以及热更版本信息(如需要)
2.在app启动时,调用更新接口,把版本信息传递给服务器
3.接口返回是否需要整包更新,以及下载地址,如果需要更新就跳转下载地址,或者自动下载app。
获取应用版本信息:
android和IOS都是用2个字段来标志版本信息。
安卓时在AndroidManifest.xml里配置。
android:versionCode=“4”
android:versionName=“1.1.1”
IOS是CFBundleShortVersionString 和 CFBundleVersion。
具体用法请自行百度,不再赘述
Android的app版本信息获取:
public static String getAppVersion()
{
try
{
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
assert(info.versionName != null);
return info.versionName;
} catch (NameNotFoundException e)
{
e.printStackTrace();
}
return null;
}
public static String getAppVerCode()
{
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
String versionCode="";
try {
packageInfo=packageManager.getPackageInfo(context.getPackageName(),0);
versionCode=packageInfo.versionCode+"";
} catch (PackageManager.NameNotFoundException e)
{
e.printStackTrace();
}
return versionCode;
}
IOS获取方式:
//获取CFBundleVersion
std::string DeviceModule::getAppVerCode()
{
NSString *versionCode = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey];
return [versionCode UTF8String];
}
//获取CFBundleShortVersionString
std::string DeviceModule::getAppVersion()
{
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@“CFBundleShortVersionString”];
return [version UTF8String];
}
热更新
获取热更新版本信息:
1.自己在js代码中定义版本信息 每次打更新包之前,自行修改
2.通过 jsb.AssetsManager获取
this._am = new jsb.AssetsManager(this.manifestUrl, this._storagePath, this.versionCompareHandle);
let srcVer= this._am.getLocalManifest().getVersion();
两种方式正常情况下都可以获取到版本信息。
热更新流程
参考官方教程
https://docs.cocos.com/creator/1.9/manual/zh/advanced-topics/assets-manager.html
我总结了下,就是以下6个步骤:
1.对比本地和远程服务器的mainfest的版本信息(version.manifest)
2.下载远程服务器资源mainfest到临时文件路径
3.对比远程服务器资源mainfest和临时文件路径mainfest的差异,生成下载文件清单
4.下载清单文件,放到临时文件路径
如果热更路径是((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : ‘/’) + ‘game-remote-asset’,
那么临时文件路径就是(jsb.fileUtils ? jsb.fileUtils.getWritablePath() : ‘/’) + ‘qygame-remote-asset_temp’
5.下载完成后,拷贝临时文件到热更文件路径
6.用remote mainfest替换local mainfest
由于creator的资源uuid是在引擎启动的时候加载的,所以热更之后,需要把热更路径的搜索优先级放在前面,然后重启引擎。保证新资源在使用。
生成热更mainfest文件
creator引擎的构建,会把纹理合图,以及游戏引用到的资源生成到res目录下。js代码也会合成,并加密脚本(项目选择加密脚本),生成在src目录下。
而这些生成的文件在项目路径下的build\jsb-link路径下。如果是jsb-defalut模式,也是在相应的目录下。
生成热更mainfest文件的脚本,就是实现把这两个路径下的资源统计,并生成对应的md5标志。
参照官方热更例子做了修改。包含一个js脚本和一个执行的bat。
hotupdate.rar (1.8 KB)
热更新常见错误和解决方案
所有热更新出错的问题,最终都是一个问题,那就是mainfest文件清单,和实际应用内的文件不匹配(脚本和资源)。
根据以往项目中的经验,总结了以下几个可能出现不匹配的原因,以及如何解决这些问题。
一,生成安装包时,mainfest文件和资源不匹配。
这也是新手或者对热更机制不熟悉的人最容易出现的问题。
解决方案,打安卓和ios包之前,先构建项目然后生成一份mainfest文件,并拷贝到项目下,然后再次构建项目,再去打安卓和ios包。以保证初始包中2者一致。
二,http下载中,文件出现不一致。
解决方案:MD5校验。
md5校验方法
c++实现:本人是通过c++实现的。然后通过jsb调用来验证文件。
js实现:md5 = crypto.createHash(‘md5’).update(fs.readFileSync(subpath)).digest(‘hex’);
脚本中是这样实现的,到项目中需要修改一下,由于js实现调试的时候多了很多引用文件,所以我是c++实现的。
三,整包更新后,mainfest文件和资源不匹配
安卓和ios整包更新,会把安装包的内容替换掉,但是会保留应用程序的可写路径资源,所以整包更新也会导致mainfest文件和资源不匹配。
解决方案:本地记录上次app运行时的版本信息,检查app版本信息不一致时,删掉热更路径和临时路径文件夹。
流程是在c++实现的,在js引擎初始化之前检查,这样少了重启引擎的步骤。
相关代码:
//检查程序版本信息
void CPlatformModule::checkAppVersion()
{
std::string perVersion = UserDefault::getInstance()->getStringForKey(“UserAppVersion”,"");
std::string curVersion = getAppVersion();
if (perVersion != curVersion)
{
//当前版本和记录版本不一样
std::string writePath = FileUtils::getInstance()->getWritablePath();
//删除热更路径文件
std::string assetPath = writePath.append("qygame-remote-asset/");
FileUtils::getInstance()->removeDirectory(assetPath);
//删除热更缓存路径文件
std::string tmpAssetPath = writePath.append("qygame-remote-asset_temp/");
FileUtils::getInstance()->removeDirectory(tmpAssetPath);
UserDefault::getInstance()->setStringForKey("UserAppVersion", curVersion);
}
}
四,用户热更过程中,服务器替换热更版本。
五,临时路径拷贝到资源路径过程中,app被强制退出。(闪退或者用户自己退出)
一,二,三都处理之后,还是有一些用户热更失败,所以猜测可能是四,五导致的。四经过验证确实是会出现,我们服务器资源更新是直接把原来文件删除,然后替换新的热更文件。
如果这时候用户正在进行热更,也是会出现资源不一致情况。所以现在更新脚本都是在用户少的情况。
针对一些未知的情况,以下还有2个终极解决方案。
终极解决方案一:app启动时,校验文件以及客户端完整性。流程就是读取mainfest文件清单,然后依次做md5校验,检查文件完整性。
这个功能我并没有实现,我用了第二种比较简单点的方式。
终极解决方案二:一键修复。流程和整包更新一样,删除掉临时路径和热更路径的资源,然后重启引擎。
参考代码:
let storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : ‘/’) + ‘game-remote-asset’);
let tmpPath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : ‘/’) + ‘game-remote-asset_temp’);
jsb.fileUtils.removeDirectory(tmpPath);
jsb.fileUtils.removeDirectory(storagePath);
gg.audio.stopMusic();
cc.game.restart();