关于热更新, 虽然这已经是第三次实现了, 但每一次都会有新的收获, 都会比上次更加完善一下.
一. App 版本号的获取
对于我们的游戏来说,其实是有两个版本配置的,一个是 versionName
, 一个是 versionCode
. versionName 只是一个显示, 类似于 1.2.3
, 而 versionCode, 它是一个纯数字的字符串 (e.g. 4083), 它可以简单的转化为 int 去对比版本大小. 那么如何去获取app对应的版本呢 ?
iOS:1
2
3
4std::string Device::getAppVersionCode()
{
return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] cStringUsingEncoding:NSUTF8StringEncoding];
}
MacOS 上获取版本号的方式与 iOS 一致.
Android:1
2
3
4
5
6
7
8
9
10
11
12public static String getAppVersionCode() {
String versionCode = "";
PackageInfo pInfo;
try {
pInfo = sActivity.getPackageManager().getPackageInfo(sActivity.getPackageName(), 0);
versionCode = ""+pInfo.versionCode;
} catch (NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return versionCode;
}
为了调用这个方法, 我们还需要额外实现一个 jni 的调用.
二. 如何修改 Mac 上 UserDefault 记录
quickx-3.3 Mac 版不再像 2.x 时将 UserDefault 保存在项目的根目录, 而是使用 NSUserDefaults
将配置保存在了系统的配置目录:
1 | ~/Library/Preferences/com.cocos.quick.apps.player.plist |
双击可以使用 XCode 打开, 修改保存完毕后并不会立刻生效, 而是需要再额外运行一条命令:
1 | killall -SIGTERM cfprefsd |
三. 如何重启游戏
这个做法有好多, 纯App层面的, 纯Lua层面的, 我们使用了一个虽然很土但却很简单的做法. 因为热更新只会更新 lua 代码和资源, 因此可以通过重启 LuaEngine, 清空三大缓存来实现重启. 在 Lua 中发送重启消息, C++ 接收到消息后销毁并重新创建 LuaEngine. 关键代码如下:
Lua:
1 | cc.Director:getInstance():getEventDispatcher():dispatchEvent(cc.EventCustom:new("NEED_RESTART_APP")) |
AppDelegate.cpp:
1 | // 监听重启事件 |
四. 代码更新策略
大体分为三类:
- 全量更新
- 相对于上一个版本增量更新
- 相对于渠道版本增量更新
全量更新指每一个热更新包内都含有项目全部代码, 如果项目体量较小的话勉强可以接收这种方案, 如果项目略大一些, 更新包就会很大, 随便更改一行代码都会有 3,4M 的更新, 实乃下下之策.
相对于上一个版本增量更新, 这样更新包就会小很多, 达到资源的最大利用, 缺点是如果版本相差较大就会需要下载多个更新包, 如果代码采用 zip 压缩的话, 需要将这些 zip (e.g. 1.1-1.2.zip
, 1.2-1.3.zip
)都加载进来.
相对于渠道版本增量更新, 这一种折中的解决方案, 对比差异是永远是基于线上渠道版本, 这种做法的优点在于游戏只用额外挂载一个 patch.zip
, 玩家也只用更新一次即可到最新版, 而代价只是会增加一些服务器更新包的存储开销.
很长一段时间内, 我们都很 low 的采用了第一种解决方案, 造成这种错误的原因在于我错误的估计了增量更新的难度, 进而放宽了自我要求, 觉得全量更新还OK balabala… 多亏了 @BinStartup 童鞋强烈地需求, 才迫使我们去考虑更优解决方案, 用我们内部的一个梗来夸赞一下 Bin 童鞋: ‘牛逼呀~’ .
五. 更新包的制作
相比于代码层面的热更新支持, 自动化的制作热更新包才是更为重要的事情, 因为这个操作会持续的发生, 并且随着更新次数的增多复杂度会剧增.
从大的层面来看, 更新包的制作主要有这么几个问题:
1. 如何标记并导出版本?
首先我们要考虑如何标记版本, 因为要做增量更新就必须得对比不同的版本, 实现的方法有好多, 你甚至可以每次都将项目拷贝一份, 用版本号来命名. 我们项目版本控制工具是 git , 因此可以使用 git tag 来标记版本号.
至于导出版本, 一开始并没有考虑到这个, 本来打算直接使用 git diff
来进行版本对比, 不幸的是我们项目中有一堆的 submodule
, git diff 并不能对比子模块的变化. 同理 git archive
也不可以, 最终我们使用了:
1 | git checkout $tag |
这样当前项目的环境就到了 $tag
这次提交, 然后将 src
目录拷贝到一个临时目录, 再切换到下一个 tag . 多次之后, 就获取到了所有版本的脚本文件了.
2. 如何对比生成 diff 文件
因为是拷贝到了临时的目录, 所以不能通过 git diff
来进行对比了. 简单搜索了下, 可选的是系统自带的 diff
命令, 不过它的结果不方便处理. 最终决定使用 python 的 filecmp
模块进行对比.
3. 其他后续处理
diff 出来的脚本问题, 我们需要加密, 和加密 src 一样调用 compile_scripts.sh
即可.
因为热更新需要的是一个 zip 文件, 我们可能会用到 zip
命令, 需要注意的一点是不能简单的使用 zip -r xxx.zip yyy
, 这样的话压缩包内会含有 yyy
目录. 正确的做法是:
1 | (cd yyy; zip -r ../xxx.zip *) |
重启的那个方案还有一些隐患, 所以没有放出具体的代码, 等修复之后会补上.
暂时就这些, 后面想到啥了再加上来.