Cocos2d-x Lua 热更新机制

什么是热更新呢

游戏上线后,玩家下载第一个版本,在运营过程中,如果需要更换UI显示或修改游戏逻辑,这个时候如果不使用热更新,就需要重新打包,然后让玩家重新下载,这样既浪费流量和时间,最重要的是用户的体验非常不好。热更新是在不重新下载客户端的情况下,更新游戏的内容,热更新一般应用在手游中。

热更新也叫不停机更新,是在游戏服务器运行期间对游戏进行更新,实现不停机修正bug、修改游戏数据等操作。

热更新通常指的是客户端的资源更新,客户端在启动后访问更新API,根据更新API的反馈下载更新资源,然后使用新的资源启动客户端,或直接使用新资源不重启客户端。此种方式可跳过AppStore的审核,避免用户频繁下载、安装、覆盖产品包。使用热更新的方式可快速修复产品bug并增加新功能。

热更新一般适用于脚本语言,因为脚本无需编译,是一种解释性语言。C++是很难热更新的,其代码只要有改动就需要重新链接编译,虽然统一接口用动态库也可以实现,不过欠缺灵活。Lua相对于C++开发的优点之一是代码可在运行时加载,基于此不仅可以在编码期间更新,也可以在版本发布后更新代码。

Cocos自身也封装了热更新的模块AssetsManagerAssetsManagerEx

AssetsManager采用的是升级包的管理方式,首先进行版本号对比,然后根据URL获取对应的升级包,解压升级包,设置资源加载路径,通过加载writepath目录下最新文件的方式来实现更新。问题是当涉及跳版本更新,或只有一个文件被改动时,用户就要下载前面全部的升级内容,升级包会越来越大。

AssetsManagerExAssetsManager的加强版,不同的是不再使用升级包的方式,而是采用单个文件拉取的方式。首先获取本地更新配置,之后与服务器的更新配置比对,得出差异文件,之后单个拉取差异文件。当本地版本大于服务器版本时,会清理掉本地更新缓存。AssetsManagerEx也有尚未解决的问题,例如多个更新序列无法并行,只能顺序启动。另外版本后期随着项目庞大配置文件几乎包含了所有的文件信息,对比文件时间的耗时会越来越长。

基本思路

  1. 登录游戏前先向服务端请求当前游戏版本号信息,与并本地版本号比对。若相同则说明无资源需更新可直接进入登录。若不同则说明有资源需更新。
  2. 客户端向服务端请求当前所有资源的列表(资源名+md5),并与本地资源列表比对,找出需要更新的资源。
  3. 更具找出需要更新的资源,客户端向服务端请求并下载。

大版本号

安装包的大版本号是指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/... 其他游戏资源

热更新流程

  1. 从服务器获取版本列表flist
  2. 检查updatemd5值是否有更新,若有则下载update.bin重新载入,并退出main,退出前注意清除对某些的引用。再次重新进入 。
  3. 检查frameworkmd5值是否有更新,若有则下载framework.bin,并提示用户重新启动。
  4. 读取本地安装目录的版本列表文件flist
  5. 比对服务器版本列表与本地版本里中的大版本号,若不一致则提示用户去APPStore下载。
  6. 读取upd目录的版本列表文件flist,若存在或flist中存放的core与安装目录列表不一致,表示用户安装了新版本,则清除整个upd目录,并将本地安装目录的flist内容写入upd目录。
  7. 比对服务器列表与本地列表中version,若版本相同则认为数据无需更新,若版本不同则与服务器的flist对行md5比对,得到需要更新的文件。
  8. 遍历需要更新的文件列表,若upd存在则校验其md5值,若md5值与服务器相同,则从待更新列表中移除,其目的是为了应对上一次更新过程中,玩家中途退出的情况。
  9. 逐个更新文件,每个文件更新完毕后,再次校验其md5值,若md5码校验失败,则重新下载此文件。
  10. 待所有文件更新完毕,重写upd文件中的flist,最后进入游戏。
Cocos2d-x Lua 热更新机制_第1张图片
热更新模块流程

入口文件

对于热更新,游戏执行后首先执行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

你可能感兴趣的:(Cocos2d-x Lua 热更新机制)