上篇说到检测Lua文件发生变化,这篇来讲怎么重载lua模块。
请配合github工程来看。关于重载lua的功能在Hotfix.lua脚本上。这个脚本有三个函数,hotfix、update_table、update_func。
function hotfix(filename)
print("start hotfix: ",filename)
local oldModule
if package.loaded[filename] then
oldModule = package.loaded[filename]
package.loaded[filename] = nil
else
print('this file nevev loaded: ',filename)
return
end
local ok,err = pcall(require, filename)
if not ok then
package.loaded[filename] = oldModule
print('reload lua file failed.',err)
return
end
local newModule = package.loaded[filename]
update_table(newModule, oldModule)
if oldModule.OnReload ~= nil then
oldModule:OnReload()
end
print('replaced succeed')
package.loaded[filename] = oldModule
end
package.loaded是lua内部用于记录哪些模块被require的表,如果一个模块已经被require,那么下次再调用require就不会生效。
所以第一步就是把package.loaded记录的旧模块清空并重新require,然后使用pcall捕捉异常,如果出现错误,package.loaded仍然赋值旧模块,并返回。网上的很多博客也就停留在这一步了。) 下一步就是更新旧模块,调用update_table,最后把package.loaded[filename] 指向旧模块。
function update_table(new_table, old_table)
assert("table" == type(new_table))
assert("table" == type(old_table))
-- Compare 2 tables, and update old table.
for key, value in pairs(new_table) do
local old_value = old_table[key]
local type_value = type(value)
if type_value == "function" then
update_func(value, old_value)
old_table[key] = value
elseif type_value == "table" then
update_table(value, old_value)
end
end
-- Update metatable.
local old_meta = debug.getmetatable(old_table)
local new_meta = debug.getmetatable(new_table)
if type(old_meta) == "table" and type(new_meta) == "table" then
update_table(new_meta, old_meta)
end
end
这个函数用于更新旧表,首先断言这两个参数的类型是不是table。
然后遍历表里面的元素,有三种情况:数据、函数、表。
最后处理元表,把旧表的元表和新表的元表拿出来,嵌套调用update_table。
考虑下面的代码,这是我工程里BaseClass.lua的一段代码:
local vtbl = {}
_class[class_type] = vtbl
setmetatable(class_type, {
__newindex = function(t,k,v)
vtbl[k] = v
end
,
--For call parent method
__index = vtbl,
})
当我对class_type表里添加元素时,实际上是往vtbl也就是class_type的元表进行操作。这样当我遍历class_type的时候,实际上是找不到我添加的元素的,因为这个元素在元表里。所以在更新表的时候,需要把新表和旧表的元表都取出来,然后再次调用update_table。
function update_func(new_func, old_func)
assert("function" == type(new_func))
assert("function" == type(old_func))
-- Get upvalues of old function.
local old_upvalue_map = {}
for i = 1, math.huge do
local name, value = debug.getupvalue(old_func, i)
if not name then break end
old_upvalue_map[name] = value
end
-- Update new upvalues with old.
for i = 1, math.huge do
local name, value = debug.getupvalue(new_func, i)
if not name then break end
print('set up value: name:',name)
local old_value = old_upvalue_map[name]
if old_value then
debug.setupvalue(new_func, i, old_value)
end
end
end
这个函数用于处理upvalue,什么是upvalue,就是一个函数使用了不在这个函数内的变量。如下代码:
local count = 1
PlayerMove.TestFunc = function()
count = count + 1
print("count:",count)
end
其中的count就是upvalue,当我更新函数的时候需要保留upvalue的值。
lua内部提供了这两个方法,我们只需要把旧函数的upvalue拿出来再赋值给新函数的upvalue就可以了。
到这里,看似热重载的功能已经完成了,但是还有一种情况没有考虑到,就是如果旧表的旧函数在其他地方已经被存了一份该怎么办? 这个时候就算你把旧表的旧函数更新为新函数了,实际上其他地方存的还是旧函数。
比如我工程里GameMain.lua的一段代码:
local function Start()
print("GameMain start...")
playerMove = PlayerMove.New()
playerMove:Start()
GameMain.playerFunc = playerMove.TestFunc
end
local function TestFunc()
print("call from self...")
GameMain.playerFunc()
print("call from playermove...")
playerMove.TestFunc()
end
当我更新旧表的函数后,playerMove.TestFunc() 的确是调用的新函数,但是GameMain.playerFunc() 还是旧函数。我的解决办法是在PlayerMove.lua增加OnReload这个函数,在这个函数对GameMain.playerFunc重新赋值,然后在hotfix方法里这么调用:
if oldModule.OnReload ~= nil then
oldModule:OnReload()
end
OnReload代码如下:
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")
GameMain.playerFunc = self.TestFunc
end
end
end
我们都知道在lua里写面向对象,当我调用 [class].New() 时,实际上是生成了一个元表是[class]的实例。调用package.load[filename] 获得的是类的类型,并不是类的实例。那么我又需要调用类的实例的方法的时候怎么办?我的解决方法是类在生成一个实例时,用一个表去记录它都生成了哪些实例。同时设置成弱引用,这样当实例销毁并执行gc后,这个记录表也会自动取消对实例的引用。
在工程里的BaseClass.lua里面如下:
class_type.instances = {}
setmetatable(class_type.instances, { __mode = "v" })
class_type.New = function(...)
local obj = {}
......
table.insert(class_type.instances,obj)
return obj
end
这就是为什么OnReload里面判断了一下是class还是instance的原因。
系列文章:
【Lua运行时热重载功能实现①】检测Lua文件发生变化
【Lua运行时热重载功能实现②】重载Lua模块、替换函数
CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
Q群:891809847