目录
摘要
本文内容大部分源于官方文档,对其中一些比较重要的内容,以及有自己想法的内容做了适度的展开,意在帮助读者更加全面的了解热更新。
资源
正式开始
为什么要使用热更新
为了绕过游戏平台对游戏漫长的审核过程,以及玩家需要从游戏平台手动更新的麻烦,从而将新的游戏内容快速推送给玩家。
C++
编写的代码都会经过编译形成库文件,库文件中的东西在程序运行过程中是不能被修改的。如果想修改,就需要重新编译成新的库文件。- 游戏平台对游戏的审核时间一般都较长,会经历
提交审核 -> 打回 -> 修改 -> 提交审核
的过程,而且在审核通过之前,这个过程可能会重复多次。这就导致游戏从提交审核到审核通过,会经过很长的时间。 - 玩家想要获取新的游戏内容,需要前往游戏平台查看是否有更新,并且手动下载更新。
之前的游戏都是使用C++
编写,不需要脚本,同时也就不需要脚本引擎。但是基于以上几点,这样做出来的游戏在更新时会遇到一些问题。
比如要增加游戏功能,就需要开发 -> 编译 -> 出包(apk, ipa等) -> 提交审核 -> 打回 -> 修改 -> 提交审核 -> 审核通过 -> 上架 -> 玩家手动更新
这样一个过程后,新功能才能推送到玩家手上。而这其中,游戏平台的审核可能已经消耗了大量时间。
- 脚本可以被动态加载,需要的时候加载进内存,不需要的时候可以释放掉。
- 脚本是解释型语言,不需要编译,在运行的时候才被解释为机器语言。
- 脚本语言使用更加灵活,学习成本更低。
基于以上几点,上面的问题就有了解决方案。
脚本对底层C++
代码进行封装,暴露接口。实际上通过脚本调用的方法,就是底层C++
对应的方法。
然后将游戏的绝大部分逻辑,都放到脚本层实现。
增加游戏功能,只需要保证游戏加载的是新的脚本。
此时,热更新就登场了。相较于上面那个冗长的步骤,推送新的游戏功能只需要开发 -> 热更新
就完成了。
热更新能做什么
之前在 Cocos Creator 热更新 [Lv.1] (1) 中有简要的描述,这里稍做一些展开。
- 游戏运行时加载的资源才可以进行更新,比如脚本,图片,音频,动画等。
- 自动检测更新,下载新资源,覆盖使用新逻辑和资源。
热更新的原理
在开发者的游戏安装到用户的手机上时,它的游戏是以 .ipa(iOS)或者 .apk(Android)形式存在的,这种应用包在安装后,它的内容是无法被修改或者添加的,应用包内的任何资源都会一直存在。所以热更新机制中,我们只能更新本地缓存到手机的可写目录下(应用存储空间或者 SD 卡指定目录),并通过 FileUtils 的搜索路径机制完成本地缓存对包内资源的覆盖。同时为了保障更新的可靠性,我们在更新过程中会首先将新版本资源放到一个临时文件夹中,只有当本次更新正常完成,才会替换到本地缓存文件夹内。如果中途中断更新或者更新失败,此时的失败版本都不会污染现有的本地缓存。
在长期多次更新的情况下,本地缓存会一直被替换为最新的版本,而应用包只有等到用户在应用商店中更新到新版本才会被修改。
热更新的机制
Cocos
默认的热更新机制并不是基于补丁包更新的机制,传统的热更新经常对多个版本之间分别生成补丁包,按顺序下载补丁包并更新到最新版本。Cocos
的热更新机制通过直接比较最新版本和本地版本的差异来生成差异列表并更新。这样即可天然支持跨版本更新,比如本地版本为 A,远程版本是 C,则直接更新 A 和 C 之间的差异,并不需要生成 A 到 B 和 B 到 C 的更新包,依次更新。所以,在这种设计思路下,新版本的文件以离散的方式保存在服务端,更新时以文件为单位下载。
热更新的流程
- 服务端保存最新版本的完整资源(开发者可以随时更新服务器)。
- 客户端发送请求和服务端版本进行比对获得差异列表。
- 从服务端下载所有新版本中有改动的资源文件。
- 用新资源覆盖旧缓存以及应用包内的文件。
官方文档中的流程图讲得挺清楚的,这里我把它翻译成比较好理解的形式。
这张图的前提条件是:
- 出了一个原生平台的安装包(apk,ipa等),其中有
project.manifest v1.0.0
和version.manifest v1.0.0
。 - 服务器端放着热更资源,其中有
project.manifest v1.0.1
和version.manifest v1.0.1
。 - 玩家安装了游戏,并且运行,检测到了更新。
project.manifest
文件的格式
{
"packageUrl" : 远程资源的本地缓存根路径
"remoteManifestUrl" : 远程资源 Manifest 文件的路径,包含版本信息以及所有资源信息
"remoteVersionUrl" : [可选项] 远程版本文件的路径,用来判断服务器端是否有新版本的资源
"version" : 资源的版本
"assets" : 所有资源列表
"key" : 资源的相对路径(相对于资源根目录)
"size" : [可选项] 文件的字节尺寸,用于快速获取进度信息
"md5" : md5 值代表资源文件的版本信息
"compressed" : [可选项] 如果值为 true,文件被下载后会自动被解压,目前仅支持 zip 压缩格式
"searchPaths" : 需要添加到 FileUtils 中的搜索路径列表
}
实际的文件内容大致会是这样,
{
"packageUrl":"http://127.0.0.1:1234/",
"remoteManifestUrl":"http://127.0.0.1:1234/project.manifest",
"remoteVersionUrl":"http://127.0.0.1:1234/version.manifest",
"version":"1.0.0",
"assets":{
"src/cocos2d-jsb.js":{"size":3291436,"md5":"6d22a3d301aa89d36100e888ce7bddfd"},
"src/physics.js":{"size":462321,"md5":"c4610bcc43c1177f88ee119b289edd0e"},
"src/settings.js":{"size":393,"md5":"d1dd282a7a8edcd185610ab54fbe59d9"},
"assets/internal/config.json":{"size":4030,"md5":"de728fed0fa036a84ac4a7e37d27cbd7"},
...
"assets/internal/import/0b/0b6c4470e.json":{"size":196166,"md5":"c6a45b9ba9336179bc7f6b90e382382f"},
"assets/main/native/e8/e851e89b-faa2-4484-bea6-5c01dd9f06e2.png":{"size":1082,"md5":"90cf45d059d0408bec327f66eae5764c"}
},
"searchPaths":[]
}
这里需要注意的是,remote信息(包括packageUrl
、remoteVersionUrl
、remoteManifestUrl
)是该manifest
所指向远程包信息,也就是说,当这个manifest
成为本地包或者缓存manifest
之后,它们才有意义。另外,md5
信息可以不是文件的md5
码,也可以是某个版本号,这完全是由用户决定的,本地和远程manifest
对比时,只要md5
信息不同,我们就认为这个文件有改动。
为什么要多一个version.manifest
首先来看看version.manifest
的格式,
{
"packageUrl" : 远程资源的本地缓存根路径
"remoteManifestUrl" : 远程资源 Manifest 文件的路径,包含版本信息以及所有资源信息
"remoteVersionUrl" : [可选项] 远程版本文件的路径,用来判断服务器端是否有新版本的资源
"version" : 资源的版本
}
再来看实际的文件内容,
{
"packageUrl":"http://127.0.0.1:1234/",
"remoteManifestUrl":"http://127.0.0.1:1234/project.manifest",
"remoteVersionUrl":"http://127.0.0.1:1234/version.manifest",
"version":"1.0.0",
}
会发现version.manifest
实际上就是精简版的project.manifest
。
其意义在于,在确认是否有新版本时,减少下载量。
例如,工程中的资源很多,那么project.manifest
文件的assets
字段就会很长,project.manifest
文件就会比较大,比如大到了几百KB或是1M。
如果每次检测是否有新版本,都需要下载project.manifest
来进行比对,那么每次运行游戏,在热更阶段无论是否有更新,都需要下载几百KB或是1M的数据,这无疑拖慢了热更的节奏。而实际上,我们只需要一个版本号来比对就可以了。
所以,将比较版本的一些必要信息提取出来,就形成了version.manifest
,其大小只有几百B。
每次将它下载下来,对比其中的版本号,就能确认是否有更新。
工程资源和游戏包内资源的区别
大家在创建一个Cocos Creator
工程的时候,可以看到它的目录下有assets
目录,里面保存了你的场景、脚本、prefab
等,对应编辑器中的assets
面板。
但是这些工程资源并不等同于打包后的资源,在使用构建面板构建原生版本时,Cocos Creator
会在构建目录下生成assets
和src
文件夹,这两个文件夹内保存的才是真正让游戏运行起来的游戏包内资源。其中src
包含所有脚本,assets
包含所有资源。
所以我们的资源热更新自然应该更新构建出来的资源,而不是工程的assets
目录。
断点续传
热更新管理器支持断点续传,并且同时支持文件级别和字节级别的断点续传。
那么具体是怎么做的呢?首先我们使用project.manifest
文件来标识每个资源的状态,比如未开始、下载中、下载成功。
在热更新过程中,文件下载完成会被标识到内存的project.manifest
中,当下载完成的文件数量每到一个进度节点(默认以 10% 为一个节点)都会将内存中的project.manifest
序列化并保存到临时文件夹中(存储为project.manifest.temp
)。
在中断之后,再次启动热更新流程时,会去检查临时文件夹中是否有未完成的更新,校验版本是否和远程匹配后,则直接使用临时文件夹中的project.manifest.temp
作为 Remote Manifest 继续更新。此时,对于下载状态为已完成的,不会重新下载,对于下载中的文件,会尝试发送续传请求给服务器(服务器需要支持Accept-Ranges
,否则从头开始下载)。
重启的必要性
如果要使用热更新之后的资源,需要重启游戏。有两个原因,第一是更新之后的脚本需要干净的JS
环境才能正常运行。第二是场景配置,AssetsLibrary
中的配置都需要更新到最新才能够正常加载场景和资源。
JS 脚本的刷新
在热更新之前,游戏中的所有脚本已经执行过了,所有的类、组件、对象已经存在 JS context 中。所以热更新之后如果不重启游戏就直接加载脚本,同名的类和对象虽然会被覆盖,但是之前旧的类创建的对象是一直存在的。而被直接覆盖的全局对象,原先的状态也被重置了,就会导致新版本和旧版本的对象混杂在一起。并且对内存也会造成额外开销。
资源配置的刷新
在Cocos2d-x/JS
中可以不重启游戏就直接使用新的贴图、字体、音效等资源。但是在Creator
中不可以,因为Creator
的场景和资源都依赖于settings.js
。如果settings.js
没有重新执行,并被main.js
和AssetsLibrary
重新读取,那么游戏中是加载不到热更新后的场景和资源的。
启用新的资源
而如何启用新的资源,就需要依赖Cocos
引擎的搜索路径机制了。Cocos
中所有文件的读取都是通过FileUtils
,而FileUtils
会按照搜索路径的优先级顺序查找文件。所以我们只要将热更新的缓存目录添加到搜索路径中并且前置,就会优先搜索到缓存目录中的资源。
更新引擎
升级游戏使用的引擎版本可能会对热更新产生巨大影响,开发者们可能有注意到在原生项目中存在src/cocos2d-jsb.js
文件,这个文件是JS
引擎编译出来的,包含了对C++
引擎的一些接口封装和JS
引擎框架。在不同版本的引擎中,它的代码会产生比较大的差异,而C++
底层也会随之发生一些改变。这种情况下,如果游戏包内的C++
引擎版本和src/cocos2d-jsb.js
的引擎版本不一致,可能会导致严重的问题,甚至游戏完全无法运行。
建议更新引擎之后,尽量推送大版本到应用商店。如果仍采用热更新方案,请一定要仔细完成各个旧版本更新到新版本的测试。
划重点
- 为什么要使用热更新
- 热更新的原理、机制、流程
- 为什么要多一个
version.manifest
- 工程资源和游戏包内资源的区别
- 重启的必要性