什么是热更新,对于它的理解,正如云风所说的那样,热更新更多的用途是做不停机的 bug 修复,不应用于常规的版本更新。对于热更新的博客,网上看了不少,包括云风写的一篇 热更文章。也仔细看了 snax 的热更部分实现细节。发现有不少可以吸取之处。并把核心部分抽取出来,做个简单分享。
至于怎么个热更新法,更新的是哪些内容,我的理解是,热更新最好只更新模块中的一小部分,比如其中的某个函数,而不是将这个模块都一起更新替换。尽量做到小改动,以达到最终目的。至于更新的思路,我归纳为两点:
- 将模块中旧的函数替换成新的函数,这个新的函数可以放到一个lua文件中,或者以字符串的形式给出。
- 将模块中旧的函数,当前用到的所有上值,(什么是上值,后面有讲到)保存到起来,用于新函数引用,保证新函数作为模块中的一部分能够正确运行。
下面以一个demo为例,这也是我抽取 snax 模块中热更新部分,以及和他人一起探讨写的。
目录结构:
./main.lua 调用 test.lua,做为运行文件,显示最终运行效果
./test.lua 一个简单模块文件,用于提供热更新的来源
./test_hot.lua 用于更新替换 test 模块中的某些函数,更新文件
./hotfix.lua 实现热更新机制
通过这幅关系图,可以了解到,test 模块和 test_hot 之间的关系,test_hot 负责更新 test 模块中的某些函数,但更新后的这些函数依然属于 test 模块中的一部分,并没有脱离 test 模块的掌控,而独立出来。
现在我们看看 test.lua 包含了哪些内容,分别有 一个局部变量 index,两个函数 print_index,show ,函数体分别是圆圈1和2,两个函数都引用到了这个局部变量 index。
假设当前,我们想更新替换掉 print_index 函数,让其 index 加1 操作,并打印 index 值,那么我们可以在 test_hot.lua 文件中这么写,见下图黄色框部分:
我们希望在 print_index 更新后, index 加 1 后,show 函数获取到的 index 值是 1,即把更新函数也看作是 test.lua 模块中的一部分。而不应该是 index 加 1 后,show 函数获取到的还是原值 0。
假设我们希望更新 print_index 后,再一次更新,把 index 值直接设置为 100,那么它又应该是这样子的,见下图最左侧黄色部分:
通过这几幅图,我们可以大致猜想到,热更新后,应该是个什么效果。
再谈及热更之前,先要介绍几过 lua 概念。一个是 _ENV 环境变量,一个是上值 upvalue。
_ENV
在 lua 程序设计一书中有过这样的解释,lua 语言并没有全局变量,所谓的全局变量都是通过某种手段模拟出来的。
Lua 语言是在一个名为 _ENV 的预定义上值(一个外部的局部变量,upvalue)存在的情况下编译所有的代码段的。因此,所有的变量要么绑定到一个名称的局部变量,要么是 _ENV 中的一个字段,而 _ENV 本身是一个局部变量。
例如:
local z = 10
x = 0
y = 1
x = y + z
等价于
local z = 10
_ENV.x = 0
_ENV.y = 1
_ENV.x = _ENV.y + z
x,y 都是不用 local 声明,z 是 local 声明。
所以,我们用到的全局变量其实是保存到 _ENV 变量中。lua 语言在内部维护了一个表来作用全局环境(_G),通常,我们在 load 一个代码段,一个模块时,lua 会用这个表(_G)来初始化 _ENV。如果上面的几行代码是写在一个文件中,那么当 load 调用它时,又会等价于:
-- xxx.lua 文件
local _ENV = the global environment (全局环境)
return function(...)
local z = 10
_ENV.x = 0
_ENV.y = 1
_ENV.x = _ENV.y +z
end
upvalue
当一个局部变量被内层的函数中使用的时候, 它被内层函数称作 上值,或是 外部局部变量。引用 Lua 5.3 参考手册
例如:
local x = 10
function hello(a, b)
local c = a + b + x
print(c)
end
那么在这段代码中,hello 函数的上值有 变量 x,_ENV,而我们刚刚讲到,print 没有经过声明,就可以直接使用,那么它肯定是保存于 _ENV 表中,print(c) 等价于 _ENV.print(c),而变量 a、b、c 都是做为 hello 函数的局部变量。
了解这个这个上值的概率,我们才能在 hotfix 模块中理解代码的含义。下面就来看下具体 demo 的实现。
-- main.lua
local hotfix = require "hotfix"
local test = require "test"
local test_hot = require "test_hot"
print("before hotfix")
for i = 1, 5 do
test.print_index() -- 热更前,调用 print_index,打印 index 的值
end
hotfix.update(test.print_index, test_hot) -- 收集旧函数的上值,用于新函数的引用,这个对应之前说的归纳第2小点
test.print_index = test_hot -- 新函数替换旧的函数,对应之前说的归纳第1小点
print("after hotfix")
for i = 1, 5 do
test.print_index() -- 打印更新后的 index 值
end
test.show() -- show 函数没有被热更,但它获取到的 index 值应该是 最新的,即 index = 5。
接下来看看 test.lua 模块内容
-- test.lua
local test = {}
local index = 0
function test.print_index()
print(index)
end
function test.show( )
print("show:", index)
end
return test
再看看 热更文件 test_hot.lua 内容
-- test_hot.lua
local index -- 这个 index 必须声明,不用赋值,才能够引用到 test 模块中的局部变量 index
return function () -- 返回一个闭包函数,这个就是要更新替换后的原型
index = index + 1
print(index)
end
最后,再看看 hotfix.lua
-- hotfix.lua
local hotfix = {}
local function collect_uv(f, uv)
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then -- 当所有上值收集完时,跳出循环
break
end
if not uv[name] then
uv[name] = { func = f, index = i } -- 这里就会收集到旧函数 print_index 所有的上值,包括变量 index
if type(value) == "function" then
collect_uv(value, uv)
end
end
i = i + 1
end
end
local function update_func(f, uv)
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then -- 当所有上值收集完时,跳出循环
break
end
-- value 值为空,并且这个 name 在 旧的函数中存在
if not value and uv[name] then
local desc = uv[name]
-- 将新函数 f 的第 i 个上值引用旧模块 func 的第 index 个上值
debug.upvaluejoin(f, i, desc.func, desc.index)
end
-- 只对 function 类型进行递归更新,对基本数据类型(number、boolean、string) 不管
if type(value) == "function" then
update_func(value, uv)
end
i = i + 1
end
end
function hotfix.update(old, new)
local uv = {}
collect_uv(old, uv)
update_func(new, uv)
end
return hotfix
这个用到了 lua 的两个 api 函数,在 Lua 5.3 参考手册 中有介绍。
debug.getupvalue (f, up)
此函数返回函数f
的第up
个上值的名字和值。 如果该函数没有那个上值,返回 nil 。
debug.upvaluejoin (f1, n1, f2, n2)
让 Lua 闭包f1
的第n1
个上值 引用 Lua 闭包f2
的第n2
个上值。
我们可以看到, hotfix.lua 做的事也是比较简单的,主要是收集 旧函数的所有上值,更新到新函数中。最后一步替换旧函数是在 main.lua 中完成。
最后看看运行结果:
[root@instance test]# lua main.lua
before hotfix
0
0
0
0
0
after hotfix
1
2
3
4
5
-------------
show: 5
在了解了热更新机制后,最后来思考几个问题
- 在热更文件 test_hot.lua 中,如果要更新的函数有很多,那么要声明的变量就会有很多,这个繁琐的事情,应该如何解决。
- 如果要更新的是 test.lua 中的局部函数,而这个局部函数又同时被多个其他函数引用到,改怎么热更,才能解决其他函数引用问题。
- hotfix.lua 中的 collect_uv 函数,目前只对上值是 function 类型,才继续递归收集上值。就有可能会有一些上值没办法继续收集到,比如表,在 test.lua 中加入如下内容,那么 cmd 中的 show 方法,就没办法收集到。
...
local cmd = {}
function cmd.show()
end
function test.getcmd(name)
local c = cmd[name]
if c then c() and
end