之前项目用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-热更新小结