资讯中心

Lua脚本热更新实战指南

📅 2026/8/22 11:04:20
Lua脚本热更新实战指南
本文续写脚本代码热更新在游戏客户端、或服务端的实现, 之前写过一篇【客户端热更新】, 里面提及热更新需注意的要点, 此篇作为续篇就不再重复讲了, 这次主要讲在那里无法热更新的闭包函数、以及怎么保留这两个遗留缺陷, 说完之后转过头看看另一个解释型动态类型语言“Lua”。想来游戏行业里的同行们, 都不会对Lua语言感到陌生, Lua有着基于寄存器的虚拟机这个特点, 还有简洁的语法, 具备高效的编译执行能力, 以及容易嵌入的特性。在国内互联网技术方面, Lua的应用占据了不少市场, 在redis等等之中, 都能够瞧见Lua那忙碌的身影。Lua的语言特性相对而言更为简单, 接下来要看一下怎样逐步达成Lua脚本的热更新。Lua的函数与的类似地Lua的()把一个lua文件加载存放到.在其中, 重复同一个模块, 实际上仍旧是沿用第一次加载的那个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能看到, 强制性使新的模块把新的代码予以更新, 极为简单且粗暴。然而, 明显地有着诸多问题, 被旧的引用所留住的模块没办法获得更新, 全局变量得利用“a a or 0”这般的约定去留存等等。这样程度的热更新顯然没法契合当下的游戏开发需求。Lua的函数在Lua 5.1里, 存在那样的函数, 这种函数能够去改变作用域。或者, 能够给函数的执行去设置一个环境表。要是不进行调用的情况下, 一段lua chunk的环境表就是_G, 而_G也就是Lua State的全局表。print、pair等这些函数, 实际上都是存储在全局表当中的。那么, 这个究竟有什么作用? 在知晓一段lua代码之后, 会历经语法解析而返回一个Proto, Lua加载任何代码chunk , 也皆会返回一个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)()做完这一步, 有些人想必已经清楚该如何去做更新了也就是针对旧环境表里的数据以及代码展开处理, 这里的细节就不逐个贴代码了, 重点在于留意处理和模拟的class的更新细节, 依据具体情形进行取舍。Lua的debug库函数Lua的函数, 是被带有词法定界的first - class value, 也就是说在Lua当中, 函数跟其他值, 像是数值、字符串这些, 是一样的, 可以当作变量, 能够存放在表里面, 可作为传参, 也能够进行返回。利用这样子来达成闭包的功能时, 内嵌的函数能够访问外部的局部变量。这一特性给Lua带来强大编程能力的同时, 它的函数不再是单一无状态的函数, 而是跟外部局部变量一起形成包含各种状态的闭包。假如热更新缺少了对这种闭包的更新, 那么可用性就会大打折扣。接下来要阐述的是, 热更新针对旧数据该如何进行处理, 以及闭包有效性问题要怎样去解决, 就在这个时候, 功能强大的Lua debug api闪亮登场了, 调用debug库之中的函数能够访问处于任何活动状态的局部变量, 函数是可以对Lua函数进行访问的, 并且还存在与之相对应的修改函数。例如这是查询和修改函数局部变量写的debug函数-- 查找函数的local变量 function get_local( func, name ) local i1 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 i1 end if v_name and v_name name then return value end return nil end -- 修改函数的local变量 function set_local( func, name, value ) local i1:l0e.ulfvw.cN local v_name while true do v_name, _ debug.getlocal(func,i) if not v_name or v_name name then break end i i1 end if not v_name then return false end debug.setlocal(func,i,value) return true end能确定下来的实际是在语法解析阶段一个函数局部变量的位置, 此时生成的是借助寄存器索引找到局部变量的, 理解这点理应不是艰涩之事, 进而轻松明白上面所作的代码。我并未列举修改之处, 同理, 此刻想必你肯定已经觉察到了, 通过这种方式能够达成某种水平的数据更新。明白了debug api的操作之后, 却依旧对于问题的解决完全没有任何头绪, 那就先来瞧瞧怎样对代码进行热更新吧, 上面所呈现的代码是我在进行修改调试之际撰写的。热更新并非是对文件在其原本位置进行修改更新, 而是首先要把即将要修改的函数制作成patch, 接着再将这个patch添加入正在运行的服务从而完成更新, 其中存在着一种机制, 该机制针对patch文件里的内容与服务里的内容做了重新映射, 以此达成原来的内容能够继续保持有效。可惜呀, 它并非打算针对所有闭包给予继承方面的支持, 仅仅是将热更新当作不停机状态下的bug修复机制来用, 而非对系统执行热升级操作。借助patch这种方式去进行热更新能够看得出来, 云风并不觉得热更新所有的闭包均是全然可靠的。对于热更新的定位, 我是较为赞同的, 不过我想要通过另外的方式达成热更新, 毕竟管理各种各样的patch这种方式看起来不够干净利落。深度递归替换所有的接下来要做的事明晰了, 递归全部的, 依照一定替换规则展开替换就行, 留意新的得设置回原本的环境表。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这是用于替换的函数, 不少项目都写过该函数, 此处不再粘贴, 不同项目还有各自定制的替换规则。另外, 需注意的是, 若重新设置了, 在遍历表格时替换一遍便可。最后, 要是大家对这个热更新特性感兴趣, 我会以写测试用例的方式将特性罗列出来, 不过得抽空去写, 估计代码量是这个热更新代码的两三倍。