什么是热更新呢
游戏上线后,玩家下载第一个版本,在运营过程中,如果需要更换UI显示或修改游戏逻辑,这个时候如果不使用热更新,就需要重新打包,然后让玩家重新下载,这样既浪费流量和时间,最重要的是用户的体验非常不好。热更新是在不重新下载客户端的情况下,更新游戏的内容,热更新一般应用在手游中。
热更新也叫不停机更新,是在游戏服务器运行期间对游戏进行更新,实现不停机修正bug、修改游戏数据等操作。
热更新通常指的是客户端的资源更新,客户端在启动后访问更新API,根据更新API的反馈下载更新资源,然后使用新的资源启动客户端,或直接使用新资源不重启客户端。此种方式可跳过AppStore的审核,避免用户频繁下载、安装、覆盖产品包。使用热更新的方式可快速修复产品bug并增加新功能。
热更新一般适用于脚本语言,因为脚本无需编译,是一种解释性语言。C++是很难热更新的,其代码只要有改动就需要重新链接编译,虽然统一接口用动态库也可以实现,不过欠缺灵活。Lua相对于C++开发的优点之一是代码可在运行时加载,基于此不仅可以在编码期间更新,也可以在版本发布后更新代码。
Cocos自身也封装了热更新的模块AssetsManager
、AssetsManagerEx
。
AssetsManager
采用的是升级包的管理方式,首先进行版本号对比,然后根据URL获取对应的升级包,解压升级包,设置资源加载路径,通过加载writepath
目录下最新文件的方式来实现更新。问题是当涉及跳版本更新,或只有一个文件被改动时,用户就要下载前面全部的升级内容,升级包会越来越大。
AssetsManagerEx
是AssetsManager
的加强版,不同的是不再使用升级包的方式,而是采用单个文件拉取的方式。首先获取本地更新配置,之后与服务器的更新配置比对,得出差异文件,之后单个拉取差异文件。当本地版本大于服务器版本时,会清理掉本地更新缓存。AssetsManagerEx
也有尚未解决的问题,例如多个更新序列无法并行,只能顺序启动。另外版本后期随着项目庞大配置文件几乎包含了所有的文件信息,对比文件时间的耗时会越来越长。
基本思路
- 登录游戏前先向服务端请求当前游戏版本号信息,与并本地版本号比对。若相同则说明无资源需更新可直接进入登录。若不同则说明有资源需更新。
- 客户端向服务端请求当前所有资源的列表(资源名+
md5
),并与本地资源列表比对,找出需要更新的资源。 - 更具找出需要更新的资源,客户端向服务端请求并下载。
大版本号
安装包的大版本号是指C++部分的版本号,若有变动此版本号才会动。用来提示用户去APPStore下载新的版本。其他的版本号,只是一个显示版本号,可以根据游戏内容来区分。
安装包内部带有一个文件列表的Lua文件,之所以使用Lua文件,是为了在Lua中使用dofile
方便读取。而files
中列出了所有包内的文件。
local flist = {
core = 1 -- 大版本号
version = "1.0.1" -- 显示版本号
update_md5 = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
framework_md5 = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
-- 安装包内文件,path是相对于res的路径,带完整目录和文件后缀。
files = {
{path="ui/imgs/close_btn.png", md5="xxxxxxxxxxxx", size="30"},
{path="ui/imgs/tips.png", md5="xxxxxxxxxxxx", size="20"}
}
}
return flist
资源服务器上也有一份同样的资料列表,服务器和安装包中的结构如下:
-
res/flist
资源列表 -
res/update.bin
update
模块的打包 -
res/framework.bin
quick-framework
的打包 -
res/game.bin
游戏逻辑的打包 -
res/...
其他游戏资源
热更新流程
- 从服务器获取版本列表
flist
- 检查
update
的md5
值是否有更新,若有则下载update.bin
重新载入,并退出main
,退出前注意清除对某些的引用。再次重新进入 。 - 检查
framework
的md5
值是否有更新,若有则下载framework.bin
,并提示用户重新启动。 - 读取本地安装目录的版本列表文件
flist
- 比对服务器版本列表与本地版本里中的大版本号,若不一致则提示用户去APPStore下载。
- 读取
upd
目录的版本列表文件flist
,若存在或flist
中存放的core
与安装目录列表不一致,表示用户安装了新版本,则清除整个upd
目录,并将本地安装目录的flist
内容写入upd
目录。 - 比对服务器列表与本地列表中
version
,若版本相同则认为数据无需更新,若版本不同则与服务器的flist
对行md5
比对,得到需要更新的文件。 - 遍历需要更新的文件列表,若
upd
存在则校验其md5
值,若md5
值与服务器相同,则从待更新列表中移除,其目的是为了应对上一次更新过程中,玩家中途退出的情况。 - 逐个更新文件,每个文件更新完毕后,再次校验其
md5
值,若md5
码校验失败,则重新下载此文件。 - 待所有文件更新完毕,重写
upd
文件中的flist
,最后进入游戏。
入口文件
对于热更新,游戏执行后首先执行main.lua
,然后调用launcher
模块代码,launcher
根据版本决定下一步的逻辑。
-- main.lua
function __G__TRACKBACK__(errmsg)
print("LUA ERROR: "..tostring(errmsg).."\n")
print(debug.traceback("", 2))
end
-- 清除文件缓存避免无法加载新资源
local fileUtils = cc.FileUtils:getInstance()
fileUtils:setPopupNotify(false) -- 文件加载失败后禁止弹出消息框
fileUtils:purgeCachedEntries() --清除搜索文件缓存,避免无法加载新的资源。
-- 热更新模块
cc.LuaLoadChunksFromZIP("code/launcher.zip")
package.loaded["launcher.launcher"] = nil
require("launchere.launcher")
热更新模块
热更新launcher
模块,先请求服务器的launcher
模块文件,如果本地launcher
模块文件和服务器不同则替换新的模块并重新加载。
模块和require机制
Lua内部提供require()
用来实现模块的加载,其主要功能:
- 在
registry["_LOADED"]
表中判断该模块是否已经加载过,若已加载过则返回,避免重复加载。 - 依次调用注册的
loader
来加载模块 - 将加载过的模块赋值给
register["_LOADED"]
表 -
register["_LOADED"]
表实际对应的是package.loaded
表
实现Lua代码热更新,其实也就是需要重新加载某模块,因此想办法让Lua认为之前从未加载过模块。Lua中的require()
会阻止多次加载相同的模块,当需要更新系统时,要卸载掉相应的模块。并把全局表中对应模块表置为nill
,同时把数据记录在专用的全局表下,并用local
去引用它。初始化数据时,应首先检查是否已被初始化过,以保证数据不被更新过程重置。
-- require()增强版,动态更新模块代码,解决已经引用模块的地方不会得到更新。
function reload(module)
-- 解决已经引用模块的不会得到更新
local ori_module = _G[module]
-- 判断是否曾经加载过此模块
if package.loaded[module] then
end
-- 将该模块原来在表中注册的值清空
package.loaded[module] = nil
-- 再次调用require进行模块加载和注册
require(module)
-- 将引用该模块的地方的值也做对应更新
local new_module = _G[module]
for k,v in pairs(new_module) do
ori_module[k] = v
end
package.loaded[module] = ori_module
end