软件开发中的热更新概述

什么是热更

所谓的热更新(本文中将其与热加载等同),就是运行时更新代码和资源。

在主动热更新过程中,客户端向服务器发送请求询问是否有更新,若服务器告知客户端没有更新,就直接进入下一流程。但如果是告知有更新,那就会进入更新流程。按照约定下载指定的文件进行客户端内容的替换和更新。

热更新也可以做成被动的,直接约定由服务器发送的某条消息触发热更流程。

为什么要热更

这里说的热更,包括开发时热更新以及产品发布后的热更新。

  1. 快速版本迭代。
  2. 减少用户手动更新 app 次数。
  3. 及时修复 bug。通过热更新及时将应用更新到最新版,快速让补丁生效,不必等到用户手动下载新版。
  4. 运行时修复 bug。对于服务器来说可以不停机完成 bug 修复和更新,提高用户使用体验。
  5. 开发时热更新可以减少等待代码生效的时间。对于大型项目来说,修改代码后的编译/启动时间经常会很长,此时使用开发时热更新可以大大提高开发效率。

对于移动App来说,只要产品还在生命周期以内,就不可避免会面临版本升级的问题。产品运营能力的提升,都是靠APP版本更新迭代,早期对于热更新的需求动机主要就是为了解决新版本升级时的升级率和版本收敛速度的问题,随着需求的不断发展,如今热更新被视为整个产品精细化运营的基础,实现版本迭代的灰度发布、AB测试、各种精细化更新。
https://zhidao.baidu.com/question/1435051092563588699.html

热更新对于游戏这种需求多变,且需要和用户保持常连接的程序是比较重要的。这样你可以在设计人员不断提出新的需求时,可以在不间断对用户的服务的基础上更新系统。当然快速修补 bug 也是个很重要的用途。
做这样的系统,关键是各种服务要拆分开。使用多进程的设计尤为重要。当的的系统中的各个模块以独立进程的子系统形式出现时,我们只需要把子系统隐藏在连接服务器后,不跟玩家直接通讯。大多数子系统都可以轻易设计成可以动态装卸的。需要更新的时候只需要重新启动一下即可。
基于 lua 的热更新系统设计要点 - 云风

为什么可以热更

很多项目采用 lua 的一大原因是 lua 可以方便的做热更新。

你可以在不中断进程运行的情况下,把修改过的代码塞到进程中,让随后的过程运行新版本的代码。这得益于 lua 的 function 是 first class 对象,换掉代码不过是在让相应的变量指向新的 function 对象而已。

为什么难以有一个通用的 Lua 开发时热更方案

正因为 lua 的这种灵活性,想把热更新代码这件事做的通用,且 100% 做对,又几乎是不太可能的。

首先,你很难准确地定义出,什么叫做更新————哪些数据需要保留,哪些需要替换成新版本。光从源代码和运行时的元信息上去分析是远远不够的。

考虑一个场景模块 MainScene.lua。此模块创建后,在初始化函数中会有一些操作,场景管理器还会调用这一模块的场景切换切换函数。
还会把自己的实例引用赋给 SceneManager.lua 对应模块的某个变量上,
方便其他模块需要知道场景信息时调用。

当我们要热更 MainScene.lua 时,简单地通过 package.loaded 和 require 的技巧换掉 MainScene.lua,不会有理想效果,初始化函数都需要手动调用,而后面的场景切换函数调用,以及引用赋值,更是需要想办法手工按照项目中约定的流程触发。

哪怕上面这些流程都做对了,热更 MainScene.lua,我们可能希望当前场景不变,也可能希望整个场景重新进入,这些方式都可以视为热更了,并且也与项目的具体流程关系密切。

另外,lua 只有一种通用数据结构 table ,这方便了我们做数据更新;但同时也制造了一些模糊性难题。比如,如果在代码中有一些常量配置数据表,写死在源代码中,通常你是希望跟着新版本一起更新的;而有一些表,记录着运行时的状态,你又不希望在代码更新后状态清空。

因此,一个通用的热更流程是很难做出来的,依据具体项目定制会可行许多。

开发时热更方案是什么

在开发期,最好就是改两行代码,能立刻让进程刷新成新版本的代码,并不中断运行。如果更新结果和预期不符,最坏的后果也不过是关掉程序重新来而已。但是如果仅仅是改两行代码,或加几行 log ,则基本不会出错,但开发效率却可以极大的提高。

热更/热加载实例

开发时通用热更的典型例子:flutter 热更

flutter 做到了通用的开发时热更。对所有 flutter 项目,都支持了更新代码后直接同步最新代码到运行时的应用上。

运行时热更的典型例子:VSCode 插件体系

在 VSCode 1.36 后,新安装的插件可以立刻生效,不必重新打开 vscode。

运行时热更的典型例子:王者荣耀

在客户端启动时,经常可以见到王者荣耀直接拉取新内容进行更新,不需要用户手动下载新版本。

在游戏对局过程中,有时也能发现“服务器正在更新”的提示(很小机率出现),一般在 1 秒以内即完成。达到了玩家在对局过程中即可更新游戏服务器,而且做到不影响玩家继续游戏。

热更新的关键

热更新的关键是:找到更新的代码模块和在内存中运行的对应模块,到底有什么差异,以及如何处理这些差异。

对于 lua 来说,函数当然是用新版本替换老版本,但是要小心的处理 upvalue。

热更的实现方案

笔者这里简要描述两种思路。

以 package 为单位,进行手动更新的方案

针对同一类模块,需要先分析出此类模块是如何初始化的、初始化之后进行了哪些操作、对系统其他部分有哪些影响。

然后根据分析结果,在 package.loaded 相应模块置 nil 再 require 后,补上相应的初始化操作和针对其影响的部分的替换操作。

这一方案对原有系统无侵入性,可以在原始代码基本上不改机制的情况下实现(但是需要原始代码模块化较好)。缺点时对于某些初始化复杂并且有复杂引用的模块,很难做出热更方案。

以模块为单位进行自动更新的方案

https://github.com/cloudwu/luareload/
这个通用方案是比较前后的代码,需要制作一个沙盒,会更新函数的 upvalue.
笔者觉得这对应用的架构有一定的要求。

来自云风:
http://www.udpwork.com/item/15940.html

总结

本文对热更新、热加载进行了概述,对这些概念进行了明确,并分析了它们的意义、列举了一些常见的热更新例子和实现方案。

参考

  1. 如何让 lua 做尽量正确的热更新 - 云风
  2. 基于 lua 的热更新系统设计要点 - 云风
  3. Node.js Web应用代码热更新的另类思路