找回密码
 立即注册
首页 资源区 代码 lua语言:模块热更新方案

lua语言:模块热更新方案

貊淀 4 天前
我们知道,lua通过package模块来管理模块的搜索和加载,当使用require首次成功加载一个模块后,模块(Table)会被存储在package.loaded中,之后的require行为会直接获取到已加载的模块缓存。
如何在程序执行时,将对模块文件的修改热更新到程序中,同时确保运行时状态的正常。这是项目开发中常见的机制需求,这里给出一个遍历模块键值替换更新的方案:将新文件使用loadfile加载进内存,遍历原Table,根据key匹配value进行替换更新。
方案需要支持对运行时状态数据的继承。
模块在内存中以Table类型存在,我们把更新前的模块称为mod,在内存中的Table称为old_t,把新加载进内存的Table称为new_t。old_t被package管理:
  1. registry.package = {
  2.         loaded = {
  3.                     mod = old_t
  4.                     -- ...
  5.           }
  6. }
复制代码
将修改后的模块文件使用loadfile加载进内存(没有内置的缓存机制,每次编译),遍历将old_t的键值替换为new_t,实现模块的更新:
  1. -- load module file
  2. local new_t
  3. if package.loaded[mod] then
  4.     local filename = package.searchpath(mod, package.path)
  5.     local f, err = loadfile(filename)
  6.     if not f then
  7.                     assert(false, string.format("loadfile err=%s", err))
  8.           end
  9.     new_t = f()
  10. end
  11. -- release old value
  12. local keys = table.allkeys(old_t)
  13. for _, k in ipairs(keys) do
  14.           old_t[k] = nil
  15. end
  16. -- update new value
  17. for k, v in pairs(new_t) do
  18.           old_t[k] = v
  19. end
复制代码
运行时状态数据的处理
我们可以约定:

  • 需要继承的数据定义在模块的域内;
  • 模块提供release方法用于处理并收集原Table中需要继承的内存数据;
  • 模块提供onload方法用于将原Table的运行时数据继承到新的模块内存中
  1. local context, inherts
  2. local old_t = package.loaded[mod]
  3. if old_t and new_t then
  4.     if old_t._release then
  5.         context, inherts = old_t._release(old_t)
  6.     end
  7. end
  8. -- inhert old_t runtime
  9. if context and inherts then
  10.     for _, key in ipairs(inherts) do
  11.                     new_t[key] = old_t[key]
  12.           end
  13. end
复制代码
给出一个符合上述热更新规范的模块设计demo:
  1. local context = {} -- TODO logic agent context
  2. local logic = {
  3.     _name = "logic",
  4.     _inherit = { "_runtime" },
  5.     _release = function(self)
  6.         return context, self._inherit
  7.     end,
  8.     _onload = function(self, _context)
  9.         print(string.format("run reload on mod %s", self._name))
  10.         self._runtime._RELOAD_VERSION = self._runtime._RELOAD_VERSION + 1
  11.     end,
  12.     _runtime = {
  13.         _RELOAD_VERSION = 1
  14.     },
  15.     _hotfixver = function(self)
  16.         print("reload version:", self._runtime._RELOAD_VERSION)
  17.     end
  18. }
  19. function logic.callfunc()
  20.     print("run callfunc. [logic]")
  21. end
  22. return logic
复制代码
当我修改本地模块文件将callfunc函数定义为:
  1. function logic.callfunc()
  2.     print("run callfunc. [logic_v2]")
  3. end
复制代码
执行:
  1. local logic = require "logic"
  2. local reload = require "reload"
  3. -- old_t
  4. logic.callfunc()
  5. reload("logic")
  6. -- new_t
  7. logic.callfunc()
  8. logic:_hotfixver()
复制代码
从输出结果可以看出,callfunc被正确更新为修改后的函数,切runtime数据被正确继承;
  1. linxx@linxx-MacBookAir hotfix % lua tsreload.lua
  2. run callfunc. [logic]
  3. run reload on mod logic.
  4. run callfunc. [logic_v2]
  5. reload version:        2
复制代码
btw,根据我们做项目的经验,简单地做一次遍历替换操作风险是低的(只需要保证更新操作过程不会让出执行权)。热更新时复杂的往往是处理中间业务数据(数据的状态和引用关系是复杂的)。
在热更新要求高的项目中,我们约定,模块的设计应该偏好无状态。即模块逻辑与数据是分离的(比如以上下文的形式传入模块),热更新操作只需要处理模块的逻辑(新老Table的键值替换),来降低热更新操作的复杂度和风险。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册