lua热更新学习

这篇博客详细介绍了Lua的热更新原理,通过一个具体的demo展示了如何在不重启服务的情况下修复bug或更新功能。作者讲解了_ENV环境变量和上值(upvalue)的概念,并提供了hotfix.lua模块来实现热更新机制,包括收集旧函数上值和更新新函数的步骤。最后,文章提出了在处理多个函数更新和局部函数热更时可能遇到的问题。
摘要由CSDN通过智能技术生成

lua热更新学习

 

什么是热更新,对于它的理解,正如云风所说的那样,热更新更多的用途是做不停机的 bug 修复,不应用于常规的版本更新。对于热更新的博客,网上看了不少,包括云风写的一篇 热更文章。也仔细看了 snax 的热更部分实现细节。发现有不少可以吸取之处。并把核心部分抽取出来,做个简单分享。
至于怎么个热更新法,更新的是哪些内容,我的理解是,热更新最好只更新模块中的一小部分,比如其中的某个函数,而不是将这个模块都一起更新替换。尽量做到小改动,以达到最终目的。至于更新的思路,我归纳为两点:

  1. 将模块中旧的函数替换成新的函数,这个新的函数可以放到一个lua文件中,或者以字符串的形式给出。
  2. 将模块中旧的函数,当前用到的所有上值,(什么是上值,后面有讲到)保存到起来,用于新函数引用,保证新函数作为模块中的一部分能够正确运行。

下面以一个demo为例,这也是我抽取 snax 模块中热更新部分,以及和他人一起探讨写的。
目录结构:

./main.lua                调用 test.lua,做为运行文件,显示最终运行效果
./test.lua                一个简单模块文件,用于提供热更新的来源
./test_hot.lua            用于更新替换 test 模块中的某些函数,更新文件
./hotfix.lua              实现热更新机制

关系结构图

通过这幅关系图,可以了解到,test 模块和 test_hot 之间的关系,test_hot 负责更新 test 模块中的某些函数,但更新后的这些函数依然属于 test 模块中的一部分,并没有脱离 test 模块的掌控,而独立出来。

test.lua 模块包含的内容

现在我们看看 test.lua 包含了哪些内容,分别有 一个局部变量 index,两个函数 print_index,show ,函数体分别是圆圈1和2,两个函数都引用到了这个局部变量 index。
假设当前,我们想更新替换掉 print_index 函数,让其 index 加1 操作,并打印 index 值,那么我们可以在 test_hot.lua 文件中这么写,见下图黄色框部分:

test 模块 print_index 第一次热更后

我们希望在 print_index 更新后, index 加 1 后,show 函数获取到的 index 值是 1,即把更新函数也看作是 test.lua 模块中的一部分。而不应该是 index 加 1 后,show 函数获取到的还是原值 0。

假设我们希望更新 print_index 后,再一次更新,把 index 值直接设置为 100,那么它又应该是这样子的,见下图最左侧黄色部分:

test 模块 print_index 第n次热更后

通过这几幅图,我们可以大致猜想到,热更新后,应该是个什么效果。

再谈及热更之前,先要介绍几过 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

在了解了热更新机制后,最后来思考几个问题

  1. 在热更文件 test_hot.lua 中,如果要更新的函数有很多,那么要声明的变量就会有很多,这个繁琐的事情,应该如何解决。
  2. 如果要更新的是 test.lua 中的局部函数,而这个局部函数又同时被多个其他函数引用到,改怎么热更,才能解决其他函数引用问题。
  3. 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

 

5人点赞

 

日记本

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值