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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348