lua语言:模块热更新方案
我们知道,lua通过package模块来管理模块的搜索和加载,当使用require首次成功加载一个模块后,模块(Table)会被存储在package.loaded中,之后的require行为会直接获取到已加载的模块缓存。
如何在程序执行时,将对模块文件的修改热更新到程序中,同时确保运行时状态的正常。这是项目开发中常见的机制需求,这里给出一个遍历模块键值替换更新的方案:将新文件使用loadfile加载进内存,遍历原Table,根据key匹配value进行替换更新。
方案需要支持对运行时状态数据的继承。
模块在内存中以Table类型存在,我们把更新前的模块称为mod,在内存中的Table称为old_t,把新加载进内存的Table称为new_t。old_t被package管理:
registry.package = {
loaded = {
mod = old_t
-- ...
}
}
将修改后的模块文件使用loadfile加载进内存(没有内置的缓存机制,每次编译),遍历将old_t的键值替换为new_t,实现模块的更新:
-- load module file
local new_t
if package.loaded[mod] then
local filename = package.searchpath(mod, package.path)
local f, err = loadfile(filename)
if not f then
assert(false, string.format("loadfile err=%s", err))
end
new_t = f()
end
-- release old value
local keys = table.allkeys(old_t)
for _, k in ipairs(keys) do
old_t[k] = nil
end
-- update new value
for k, v in pairs(new_t) do
old_t[k] = v
end
运行时状态数据的处理
我们可以约定:
-
需要继承的数据定义在模块的域内;
-
模块提供release方法用于处理并收集原Table中需要继承的内存数据;
-
模块提供onload方法用于将原Table的运行时数据继承到新的模块内存中
local context, inherts
local old_t = package.loaded[mod]
if old_t and new_t then
if old_t._release then
context, inherts = old_t._release(old_t)
end
end
-- inhert old_t runtime
if context and inherts then
for _, key in ipairs(inherts) do
new_t[key] = old_t[key]
end
end
给出一个符合上述热更新规范的模块设计demo:
local context = {} -- TODO logic agent context
local logic = {
_name = "logic",
_inherit = { "_runtime" },
_release = function(self)
return context, self._inherit
end,
_onload = function(self, _context)
print(string.format("run reload on mod %s", self._name))
self._runtime._RELOAD_VERSION = self._runtime._RELOAD_VERSION + 1
end,
_runtime = {
_RELOAD_VERSION = 1
},
_hotfixver = function(self)
print("reload version:", self._runtime._RELOAD_VERSION)
end
}
function logic.callfunc()
print("run callfunc. [logic]")
end
return logic
当我修改本地模块文件将callfunc函数定义为:
function logic.callfunc()
print("run callfunc. [logic_v2]")
end
执行:
local logic = require "logic"
local reload = require "reload"
-- old_t
logic.callfunc()
reload("logic")
-- new_t
logic.callfunc()
logic:_hotfixver()
从输出结果可以看出,callfunc被正确更新为修改后的函数,切runtime数据被正确继承;
linxx@linxx-MacBookAir hotfix % lua tsreload.lua
run callfunc. [logic]
run reload on mod logic.
run callfunc. [logic_v2]
reload version: 2
btw,根据我们做项目的经验,简单地做一次遍历替换操作风险是低的(只需要保证更新操作过程不会让出执行权)。热更新时复杂的往往是处理中间业务数据(数据的状态和引用关系是复杂的)。
在热更新要求高的项目中,我们约定,模块的设计应该偏好无状态。即模块逻辑与数据是分离的(比如以上下文的形式传入模块),热更新操作只需要处理模块的逻辑(新老Table的键值替换),来降低热更新操作的复杂度和风险。
本文来自博客园,作者:linxx-,转载请注明原文链接:https://www.cnblogs.com/linxx-/p/18221342