之前项目用Lua的模块很少,确实没关注是否在客户端部分实现热重载。因为项目的服务器开发是C++和Lua的组合,在配合处理开发的时候,服务器脚本实现热重载。在客户端使用Lua的模块越来越多,也有人更多的同事开始用Lua开发。为了提高开发效率,觉得还是可以花点时间在客户端实现下Lua热重载。
Lua的特点:基于寄存器的虚拟机,简洁的语法,高效的编译执行,容易嵌入的特性。Lua在国内互联网技术上的应用也占领不少市场,redis,openresty, skynet等等都能看到Lua忙碌的身影。
一、原理
函数requier在表中package.loaded中检查模块是否已被加载。
最简单粗暴的热更新就是将package.loaded[modelname]的值置为nil,强制重新加载:
| 1 2 3 4
 | function reload_module_obsolete(module_name)     package.loaded[module_name] = nil     require(module_name) end
 | 
这样就能解决当个界面对应的Lua文件的热重载,因为有Lua对于命名有规则要求。在界面输入对面界面的Lua名称,根据配置表读取到对应的路径。当重载的界面在打开的情况下,需要关闭在重新打开才能更新对应的变化内容(是基类的实例化,对应引用没办法更新)。
实现范围仅限于单个界面的Lua脚本更新,要在GM输入对应的修改Lua脚本名称。
二、迭代后
当一些常量枚举的表更新值后,希望不要让Unity,重新Play。因为在这些表在_G(Lua的全局变量表)中,就可以根据对应的表名实现重载。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14
 | function ReloadUtil.Reload_Module(module_name)     local old_module = _G[module_name]     package.loaded[module_name] = nil     require (module_name)     local new_module = _G[module_name]     for k, v in pairs(new_module) do         old_module[k] = v     end     package.loaded[module_name] = old_module end
 | 
对于表中的K的V进行更新,使用于修改和新增,删除的情况,一般来说基本没有,都不使用了,这个值就不进行更新了。
这个时候根据文件夹和文件名实现了自动热重载,但是还有一些单例的脚本没办法更新。使用仍然有限制使用的范围。
如何自动监听文件修改,我会单独写一篇来解释。一个是C#基于FileSystemWatcher,一个是Unity的AssetPostprocessor
三、重启Lua虚拟机更新
这样的处理方式有点简单粗暴,但是没啥问题。这个方案之前也构思过。因为Lua有一些数据要做持久的缓存,就难以这个处理。为了处理在5点后开启的活动,同时减少服务器上线的推送压力。客户端根据配置主动请求相关的数据,这样对于数据请求的接口有要求和规范了。
目前这个版本调整完以后,在客户端加入根据的修改的文件类型判断,自动重启Lua虚拟机的方式,开发效率会更高一点。
四、建立一张新的全局表与旧的_G作比较
想了不适合当前项目,项目以C#主,少量的Lua。也探究了其中的原理。
| 1 2 3 4 5 6 7 8 9 10
 | 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   setfenv(func, Old)()
 | 
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设置一个空的环境表。同时,我们可以保留旧的环境表来保证之前的引用有效。
| 1 2 3 4 5 6 7 8 9 10 11
 | for name,value in pairs(env) do     local g_value = _G[name]     if type(g_value) ~= type(value) then         _G[name] = value     elseif type(value) == 'function' then         update_func(value, g_value, name, 'G'..'  ')         _G[name] = value     elseif type(value) == 'table' then         update_table(value, g_value, name, 'G'..'  ')     end end
 | 
旧环境表里的数据和代码做处理,主要是注意处理function和模拟的class的更新细节
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
 | function update_func(env_f, g_f, name, deep)          local old_upvalue_map = {}     for i = 1, math.huge do         local name, value = debug.getupvalue(g_f, i)         if not name then break end         old_upvalue_map[name] = value     end          for i = 1, math.huge do         local name, value = debug.getupvalue(env_f, i)         if not name then break end         local old_value = old_upvalue_map[name]         if old_value then             if type(old_value) ~= type(value) then                 debug.setupvalue(env_f, i, old_value)             elseif type(old_value) == 'function' then                 update_func(value, old_value, name, deep..'  '..name..'  ')             elseif type(old_value) == 'table' then                 update_table(value, old_value, name, deep..'  '..name..'  ')                 debug.setupvalue(env_f, i, old_value)             else                 debug.setupvalue(env_f, i, old_value)             end         end     end end
 | 
如果当前值为table,我们遍历table值进行对比
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
 | local protection = {     setmetatable = true,     pairs = true,     ipairs = true,     next = true,     require = true,     _ENV = true, } local visited_sig = {} function update_table(env_t, g_t, name, deep)          if protection[env_t] or protection[g_t] then return end          if env_t == g_t then return end     local signature = tostring(g_t)..tostring(env_t)     if visited_sig[signature] then return end     visited_sig[signature] = true          for name, value in pairs(env_t) do         local old_value = g_t[name]         if type(value) == type(old_value) then             if type(value) == 'function' then                 update_func(value, old_value, name, deep..'  '..name..'  ')                 g_t[name] = value             elseif type(value) == 'table' then                 update_table(value, old_value, name, deep..'  '..name..'  ')             end         else             g_t[name] = value         end     end          local old_meta = debug.getmetatable(g_t)     local new_meta = debug.getmetatable(env_t)     if type(old_meta) == 'table' and type(new_meta) == 'table' then         update_table(new_meta, old_meta, name..'s Meta', deep..'  '..name..'s Meta'..'  ' )     end end
 | 
模拟的class的更新细节
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
 | local function OnReload(self)     print('call onReload from: ',self.__cname)     if self.__ctype == ClassType.class then         print("this is a class not a instance")         for k,v in pairs(self.instances) do             print("call instance reload: ",k)             if v.OnReload ~= nil then                 v:OnReload()             end         end     else         if self.__ctype == ClassType.instance then             print("this is a instance")                 oldFunc = self.oldFunc         end     end end
 | 
详细代码
五、管理每一个Lua文件的加载
 为了每个要重载的Lua文件,以model为名放到changeList的表中。
在 reload 前建立一个沙盒。让 reload 过程不要溢出沙盒。一旦有这种情况至少调用者可以知道。
约束比较简单,就是只更新函数,不更新除函数以外的东西
可能会有的问题:
- 不用 upvaluejoin 是不能将 upvalue 关联对的。只有 upvalue 是 table 且运行时不会修改 upvalue 才可以正确运行。
- 遍历 VM 不周全。没有遍历 userdata ,没有遍历 thread 调用栈。针对 5.1 来说,还需要遍历函数的 env 。
- 简单遍历 module table 是不能保证找到所有 module 相关的函数的。
详细代码
作者相应的博客文章【Lua热更新原理】
六、关于热更新涉及的点
- upvalue
- getupvalue (f, up), setupvalue (f, up, value)
- _G和debug.getregistry
- getfenv(object) ,setfenv(function,_ENV)
附
参考:
1.cloudwu/luareload
2.如何让 lua 做尽量正确的热更新
3.【reload script】lua客户端脚本热更
4.Lua脚本热更新
5.Lua-热更新小结