Lua脚本热更新

本文继续来写一下脚本代码的热更新在游戏客户端或服务端的实现,之前写了一篇【客户端python热更新】里面提到热更新要注意的要点,这篇作为续篇就不再复述了,这次主要讲一下在python里无法热更新的闭包函数upvalue怎么保留这两个遗留缺陷,转过头来看看另外一个解释型动态类型语言"Lua"。

相信游戏行业的同行都不会对Lua语言陌生,Lua的特点:基于寄存器的虚拟机,简洁的语法,高效的编译执行,容易嵌入的特性。Lua在国内互联网技术上的应用也占领不少市场,redis,openresty, skynet等等都能看到Lua忙碌的身影。Lua的语言特性相对于python来说更加简单,接下来看一下怎么逐步实现Lua脚本的热更新。

Lua的require函数

与phthon的import类似地,Lua的require(modelname)把一个lua文件加载存放到package.loaded[modelname]中,重复require同一个模块实际还是沿用第一次加载的chunk。因此,很容易想到,第一个版本的热更新模块可以写成这样:

--强制重新载入module  
function require_ex( _mname )  
    log( string.format("require_ex = %s", _mname) )  
    if package.loaded[_mname] then  
        log( string.format("require_ex module[%s] reload", _mname))  
    end  
    package.loaded[_mname] = nil  
    require( _mname )  
end  

可以看到,强制地require新的模块来更新新的代码,非常简单暴力。但是,显然问题很多,旧的引用住的模块无法得到更新,全局变量需要用"a = a or 0"这种约定来保留等等。这种程度的热更新显然不能满足现在的游戏开发需求。

Lua的setenv函数

setenv是Lua 5.1中可以改变作用域的函数,或者可以给函数的执行设置一个环境表,如果不调用setenv的话,一段lua chunk的环境表就是_G,即Lua State的全局表,print,pair,require这些函数实际上都存储在全局表里面。那么这个setenv有什么用呢?我们知道loadstring一段lua代码以后,会经过语法解析返回一个Proto,Lua加载任何代码chunk或function都会返回一个Proto,执行这个Proto就可以初始化我们的lua chunk。为了让更新的时候不污染_G的数据,我们可以给这个Proto设置一个空的环境表。同时,我们可以保留旧的环境表来保证之前的引用有效。

local Old = package.loaded[PathFile]  
local func, err = loadfile(PathFile)  
--先缓存原来的旧内容  
local OldCache = {}  
for k,v in pairs(Old) do  
     OldCache[k] = v  
     Old[k] = nil  
end  
--使用原来的module作为fenv,可以保证之前的引用可以更新到  
setfenv(func, Old)()  

做完这一步,相信有些人已经懂得如何去做更新了,就是对旧环境表里的数据和代码做处理,这里的细节就不一一贴代码了,主要是注意处理function和模拟的class的更新细节,根据具体情况进行取舍。

Lua的debug库函数

Lua的函数是带有词法定界的first-class value,即Lua的函数与其他值(数值、字符串)一样,可以作为变量、存放在表中、作为传参或返回。通过这样实现闭包的功能,内嵌的函数可以访问外部的局部变量。这一特性给Lua带来强大的编程能力同时,其函数也不再是单一无状态的函数,而是连同外部局部变量形成包含各种状态的闭包。如果热更新缺少了对这种闭包的更新,那么可用性就大打折扣。
下面讲一下热更新如何处理旧的数据,还有闭包的upvalue的有效性问题怎么解决。这时候强大的Lua debug api上场了,调用debug库的getlocal函数可以访问任何活动状态的局部变量,getupvalue函数可以访问Lua函数的upvalues,还有相对应的修改函数。
例如,这是查询和修改函数局部变量写的debug函数:

-- 查找函数的local变量  
function get_local( func, name )  
    local i=1  
    local v_name, value  
    while true do  
        v_name, value = debug.getlocal(func,i)  
        if not v_name or v_name == name then  
            break  
        end  
        i = i+1  
    end  
    if v_name and v_name == name then  
        return value  
    end  
    return nil  
end  
-- 修改函数的local变量  
function set_local( func, name, value )  
    local i=1  
    local v_name  
    while true do  
        v_name, _ = debug.getlocal(func,i)  
        if not v_name or v_name == name then  
            break  
        end  
        i = i+1  
    end  
    if not v_name then  
        return false  
    end  
    debug.setlocal(func,i,value)  
    return true  
end  

一个函数的局部变量的位置实际上在语法解析阶段就已经能确定下来了,这时候生成的opcode就是通过寄存器的索引来找到局部变量的,了解这一点应该很容易理解上面的代码。修改upvalue的我就不列举了,同样的道理,这时你一定已经看出来了,这种方式可以实现某种程度的数据更新。

明白了debug api操作后,还是对问题的解决毫无头绪,先看看skynet怎么对代码进行热更新的吧,上面的代码是我对skynet进行修改调试时候写的。skynet的热更新并不是对文件原地修改更新,而是先把将要修改的函数打成patch,再把patch inject进正在运行的服务完成更新,skynet里面有一个机制对patch文件中的upvalue与服务中的upvalue做了重新映射,实现原来的upvalue继续有效。可惜它并不打算对所有闭包upvalue做继承的支持,skynet只是把热更新用作不停机的bug修复机制,而不是系统的热升级。通过inject patch的方式热更新可以看出来,云风并不认为热更新所有的闭包是完全可靠的。对热更新的定位我比较赞同,但是我想通过另外方式完成热更新,毕竟管理各种patch的方式显得不够干净。

深度递归替换所有的upvalue

接下来要做的事情很清晰了,递归所有的upvalue,根据一定的替换规则替换就可以,注意新的upvalue需要设置回原来的环境表。

function UpdateUpvalue(OldFunction, NewFunction, Name, Deepth)  
     local OldUpvalueMap = {}  
     local OldExistName = {}  
     -- 记录旧的upvalue表  
     for i = 1, math.huge do  
          local name, value = debug.getupvalue(OldFunction, i)  
          if not name then break end  
          OldUpvalueMap[name] = value  
          OldExistName[name] = true  
     end  
     -- 新的upvalue表进行替换  
     for i = 1, math.huge do  
          local name, value = debug.getupvalue(NewFunction, i)  
          if not name then break end  
          if OldExistName[name] then  
               local OldValue = OldUpvalueMap[name]  
               if type(OldValue) ~= type(value) then          -- 新的upvalue类型不一致时,用旧的upvalue  
                    debug.setupvalue(NewFunction, i, OldValue)  
               elseif type(OldValue) == "function" then     -- 替换单个函数  
                    UpdateOneFunction(OldValue, value, name, nil, Deepth.."    ")  
               elseif type(OldValue) == "table" then          -- 对table里面的函数继续递归替换  
                    UpdateAllFunction(OldValue, value, name, Deepth.."    ")  
                    debug.setupvalue(NewFunction, i, OldValue)  
               else  
                    debug.setupvalue(NewFunction, i, OldValue)     -- 其他类型数据有改变,也要用旧的  
               end  
          else  
               ResetENV(value, name, "UpdateUpvalue", Deepth.."    ")     -- 对新添加的upvalue设置正确的环境表  
          end  
     end  
end  

这是替换upvalue的函数,替换fucntion的函数相信很多项目都有写过,这里不再粘贴,而且不同的项目相信还有一些自己定制的替换规则。还有一点要注意的是,如果重新设置了metatable,在遍历table的时候也替换一遍就可以了。最后,如果大家对这个热更新的特性有兴趣,我会写测试用例的方式把特性罗列出来,不过得抽时间写,估计代码量是这个热更新代码的两三倍。

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

推荐阅读更多精彩内容