我们知道,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

运行时状态数据的处理

我们可以约定:

  1. 需要继承的数据定义在模块的域内;

  2. 模块提供release方法用于处理并收集原Table中需要继承的内存数据;

  3. 模块提供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