Lua热更新机制(上)
Lua热更新机制
一个Lua热更新demo
Lua在游戏开发中能广泛使用不仅由于其轻量易嵌入的特性,还有一个重要的点是易于热更新,设想在产品线上运营过程中,出现bug需要修复,频繁停机对于产品体验影响大,也影响口碑;所以实际运营我们是希望能尽量避免停止服务进行代码更新的操作,下面先从一段比较简单的代码看Lua的热更新机制:
require "mymodule" local last_file = io.popen("stat -c %Y mymodule.lua") local last_update_time = last_file:read() local old_str = string while true do local file = io.popen("stat -c %Y mymodule.lua") local update_time = file:read() os.execute("sleep 1") if update_time ~= nil then if tonumber(update_time) - tonumber(last_update_time) > 5 then last_update_time = update_time package.loaded["mymodule"] = nil require("mymodule") local new_str = string print(old_str,new_str) end end end
从上述代码可以看到:
当前代码段require了mymodule模块,轮询mymodule.lua这个文件,当发现文件更新了之后,则将package.loaded的对应项设置为nil,并重新require,就可以实现将mymodule最新的代码加载进来。
这是最朴素的的热更新机制,中间还会有很多问题,无法实际用于现网环境,在此之前,我们先看看Lua的require机制的原理。
Lua require机制
Lua中的Module
所谓module,即为了实现代码复用的模块,类似与C++的.so,在Lua中module是通过table实现的,可以是一个lua文件,也可以是一个代码的chunk。
关于require函数
我们在用lua编写了一个module之后,别的地方需要使用这个module的代码,可以通过require函数将这个module加载进来,reuqire函数原型如下:
static int ll_require (lua_State *L) { const char *name = luaL_checkstring(L, 1); lua_settop(L, 1); /* LOADED table will be at index 2 */ lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); lua_getfield(L, 2, name); /* LOADED[name] */ if (lua_toboolean(L, -1)) /* is it there? */ return 1; /* package is already loaded */ /* else must load package */ lua_pop(L, 1); /* remove 'getfield' result */ findloader(L, name); lua_rotate(L, -2, 1); /* function <-> loader data */ lua_pushvalue(L, 1); /* name is 1st argument to module loader */ lua_pushvalue(L, -3); /* loader data is 2nd argument */ /* stack: ...; loader data; loader function; mod. name; loader data */ lua_call(L, 2, 1); /* run loader to load module */ /* stack: ...; loader data; result from loader */ if (!lua_isnil(L, -1)) /* non-nil return? */ lua_setfield(L, 2, name); /* LOADED[name] = returned value */ else lua_pop(L, 1); /* pop nil */ if (lua_getfield(L, 2, name) == LUA_TNIL) { /* module set no value? */ lua_pushboolean(L, 1); /* use true as result */ lua_copy(L, -1, -2); /* replace loader result */ lua_setfield(L, 2, name); /* LOADED[name] = true */ } lua_rotate(L, -2, 1); /* loader data <-> module result */ return 2; /* return module result and loader data */ }
当我们调用require函数本质上是被映射到ll_require接口,执行的是加载模块的逻辑,所有已加载模块都被注册在表LUA_LOADED_TABLE
中,所以调用时会先去查这个tablelua_getfield
,若是这个table没有,则先找到对应加载器findloader
,加载器则从指定路径去搜索这个模块,通过接口loader去加载,并在package.loaded这个表进行注册。在这里用package.loaded表进行记录,一个是效率考虑,一个是避免相互require,导致死循环。
loader接口加载时,加载路径记录在package.path
下面,从本例可以看到:
require "mymodule" print(package.path)
执行结果如下所示:
[root]# lua test.lua ./?.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;/usr/lib64/lua/5.1/?.lua;/usr/lib64/lua/5.1/?/init.lua
可见默认搜索路径包括当前目录,也包括lua库所在的文件,且模块名会替换调路径中的?
若是我们想引用其他路径下的文件,可以通过如下方式将搜索路径放到package.path中:
require "mymodule" package.path=package.path..";./test/?.lua" require "module2"
执行结果如下:
[root /lua_test/hotfix_test]# lua test.lua ./?.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;/usr/lib64/lua/5.1/?.lua;/usr/lib64/lua/5.1/?/init.lua;./test/?.lua
可以看到test/?.lua被添加到路径之后可以正常找到test/module2.lua这个文件并加载。
关于模块的加载器
在require接口的实现我们看到,当去加载一个module时,发现这个module不在表LUA_LOADED_TABLE
中时,会先调用接口findloader
去查找对应的加载器,这个findloader
实现如下:
static void findloader (lua_State *L, const char *name) { /* push 'package.searchers' to index 3 in the stack */ lua_getfield(L, lua_upvalueindex(1), "searchers"); /* iterate over available searchers to find a loader */ for (i = 1; ; i++) { if (l_unlikely(lua_rawgeti(L, 3, i) == LUA_TNIL)) { /* no more searchers? */ lua_pop(L, 1); /* remove nil */ luaL_buffsub(&msg, 2); /* remove prefix */ } lua_pushstring(L, name); lua_call(L, 1, 2); /* call it */ } }
这个findloader
也是先拿到searchers
这个table的指针,这个table里面有如下内容:
static void createsearcherstable (lua_State *L) { static const lua_CFunction searchers[] = {searcher_preload, searcher_Lua, searcher_C, searcher_Croot, NULL}; ... }
则findloader依次执行如下接口:
1)searcher_preload
:此接口首先去查表LUA_PRELOAD_TABLE
,这里面实际村的是函数指针,不为空的话就是一个函数加载器,使用此加载器加载文件;
2)searcher_Lua
:在本例实际searcher_preload为空,是通过接口searcher_Lua
进行文件的加载。
static int searcher_Lua (lua_State *L) { const char *filename; const char *name = luaL_checkstring(L, 1); filename = findfile(L, name, "path", LUA_LSUBSEP); if (filename == NULL) return 1; /* module not found in this path */ return checkload(L, (luaL_loadfile(L, filename) == LUA_OK), filename); }
在searcher_Lua
中,
1)findfile
:是在path路径进行指定lua文件的查找;
2)luaL_loadfile
:找到之后本质上是执行了luaL_loadfile
将加载的文件重新加载到全局表中。
上面demo的一些问题
从分析加载器源码我们可以看到上述demo的一些问题:
1)对于定义在模块内的全局变量,重新加载时由于会重新执行lua模块,模块内全局变量若是在其他地方被修改,则这种修改会被丢失,源码如下:
-- test.lua require "mymodule" local last_file = io.popen("stat -c %Y mymodule.lua") local last_update_time = last_file:read() local old_str = string string2 = "cccc" print("string2:"..string2) while true do local file = io.popen("stat -c %Y mymodule.lua") local update_time = file:read() os.execute("sleep 1") if update_time ~= nil then if tonumber(update_time) - tonumber(last_update_time) > 5 then last_update_time = update_time package.loaded["mymodule"] = nil require("mymodule") local new_str = string print("string2:"..string2) print(old_str,new_str) end end end --mymodule.lua string = "aaabbbcdmmmccqqq" string2 = "bbbcdmmmcc"
执行结果如下:
[root lua_test/hotfix_test]# lua test.lua string2:cccc string2:bbbcdmmmcc aaabbbcdmmmcc aaabbbcdmmmccqqq
string2的修改,在热加载过程被重置了。
2)闭包里面的upvalue在热更过程中不会修改,如下所示:
--upvalue_test.lua mod1 = require "module2" foo_closure = mod1.foo() local old_file = io.popen("stat -c %Y module2.lua") local old_time = old_file:read() while true do foo_closure() os.execute("sleep 4") local file = io.popen("stat -c %Y module2.lua") local update_time = file:read() if tonumber(update_time) - tonumber(old_time) > 5 then package.loaded["module2"] = nil mod1 = require "module2" old_time = update_time foo_clo2 = mod1.foo() end foo_closure() if foo_clo2 ~= nil then foo_clo2() end end --module2.lua(修改前) local M = {} function M.foo() local foo_count = 1 local function add_foo() foo_count = foo_count+1 print(foo_count) end return add_foo end return M -- module2.lua(修改后) local M = {} function M.foo() local foo_count = 1 local function add_foo() foo_count = foo_count+2 print(foo_count) end return add_foo end return M
执行结果如下:
[root /lua_test/hotfix_test]# lua upvalue_test.lua 2 3 4 5 6 7 8 9 3 10 11 5 12
可以看到foo_closure这个闭包是在更新前创建了,更新后闭包里面局部变量状态被保留了,但是逻辑也是没法更新的。
参考资料
https://john.js.org/2020/10/27/Lua-Runtime-Hotfix/
https://blog.51cto.com/bosswanghai/1832133
https://blog.csdn.net/Yueya_Shanhua/article/details/52241544