Lua熱過載
之前專案用Lua的模組很少,確實沒關注是否在客戶端部分實現熱過載。因為專案的伺服器開發是C++和Lua的組合,在配合處理開發的時候,伺服器指令碼實現熱過載。在客戶端使用Lua的模組越來越多,也有人更多的同事開始用Lua開發。為了提高開發效率,覺得還是可以花點時間在客戶端實現下Lua熱過載。
Lua的特點:基於暫存器的虛擬機器,簡潔的語法,高效的編譯執行,容易嵌入的特性。Lua在國內網際網路技術上的應用也佔領不少市場,redis,openresty, skynet等等都能看到Lua忙碌的身影。
一、原理
函式requier在表中package.loaded中檢查模組是否已被載入。
function reload_module_obsolete(module_name)
package.loaded[module_name] = nil
require(module_name)
end
這樣就能解決當個介面對應的Lua檔案的熱過載,因為有Lua對於命名有規則要求。在介面輸入對面介面的Lua名稱,根據配置表讀取到對應的路徑。當過載的介面在開啟的情況下,需要關閉在重新開啟才能更新對應的變化內容(是基類的例項化,對應引用沒辦法更新)。
實現範圍僅限於單個介面的Lua指令碼更新,要在GM輸入對應的修改Lua指令碼名稱。
二、迭代後
當一些常量列舉的表更新值後,希望不要讓Unity,重新Play。因為在這些表在_G(Lua的全域性變量表)中,就可以根據對應的表名實現過載。
--這樣做雖然能完成熱更,但問題是已經引用了該模組的地方不會得到更新, 因此我們需要將引用該模組的地方的值也做對應的更新。 function ReloadUtil.Reload_Module(module_name) local old_module = _G[module_name] package.loaded[module_name] = nil require (module_name) local new_module = _G[module_name] for k, v in pairs(new_module) do old_module[k] = v end package.loaded[module_name] = old_module end
對於表中的K的V進行更新,使用於修改和新增,刪除的情況,一般來說基本沒有,都不使用了,這個值就不進行更新了。
這個時候根據資料夾和檔名實現了自動熱過載,但是還有一些單例的指令碼沒辦法更新。使用仍然有限制使用的範圍。
如何自動監聽檔案修改,我會單獨寫一篇來解釋。一個是C#基於FileSystemWatcher,一個是Unity的AssetPostprocessor
三、重啟Lua虛擬機器更新
這樣的處理方式有點簡單粗暴,但是沒啥問題。這個方案之前也構思過。因為Lua有一些資料要做持久的快取,就難以這個處理。為了處理在5點後開啟的活動,同時減少伺服器上線的推送壓力。客戶端根據配置主動請求相關的資料,這樣對於資料請求的介面有要求和規範了。
目前這個版本調整完以後,在客戶端加入根據的修改的檔案型別判斷,自動重啟Lua虛擬機器的方式,開發效率會更高一點。
四、建立一張新的全域性表與舊的_G作比較
想了不適合當前專案,專案以C#主,少量的Lua。也探究了其中的原理。
local Old = package.loaded[PathFile]
local func, err = loadfile(PathFile)
--先快取原來的舊內容
local OldCache = {}
for k,v in pairs(Old) do
OldCache[k] = v
Old[k] = nil
end
--使用原來的module作為fenv,可以保證之前的引用可以更新到
setfenv(func, Old)()
setenv是Lua 5.1中可以改變作用域的函式,或者可以給函式的執行設定一個環境表,如果不呼叫setenv的話,一段lua chunk的環境表就是_G,即Lua State的全域性表,print,pair,require這些函式實際上都儲存在全域性表裡面。那麼這個setenv有什麼用呢?我們知道loadstring一段lua程式碼以後,會經過語法解析返回一個Proto,Lua載入任何程式碼chunk或function都會返回一個Proto,執行這個Proto就可以初始化我們的lua chunk。為了讓更新的時候不汙染_G的資料,我們可以給這個Proto設定一個空的環境表。同時,我們可以保留舊的環境表來保證之前的引用有效。
for name,value in pairs(env) do
local g_value = _G[name]
if type(g_value) ~= type(value) then
_G[name] = value
elseif type(value) == 'function' then
update_func(value, g_value, name, 'G'..' ')
_G[name] = value
elseif type(value) == 'table' then
update_table(value, g_value, name, 'G'..' ')
end
end
舊環境表裡的資料和程式碼做處理,主要是注意處理function和模擬的class的更新細節
function update_func(env_f, g_f, name, deep)
--取得原值所有的upvalue,儲存起來
local old_upvalue_map = {}
for i = 1, math.huge do
local name, value = debug.getupvalue(g_f, i)
if not name then break end
old_upvalue_map[name] = value
end
--遍歷所有新的upvalue,根據名字和原值對比,如果原值不存在則進行跳過,如果為其它值則進行遍歷env類似的步驟
for i = 1, math.huge do
local name, value = debug.getupvalue(env_f, i)
if not name then break end
local old_value = old_upvalue_map[name]
if old_value then
if type(old_value) ~= type(value) then
debug.setupvalue(env_f, i, old_value)
elseif type(old_value) == 'function' then
update_func(value, old_value, name, deep..' '..name..' ')
elseif type(old_value) == 'table' then
update_table(value, old_value, name, deep..' '..name..' ')
debug.setupvalue(env_f, i, old_value)
else
debug.setupvalue(env_f, i, old_value)
end
end
end
end
如果當前值為table,我們遍歷table值進行對比
local protection = {
setmetatable = true,
pairs = true,
ipairs = true,
next = true,
require = true,
_ENV = true,
}
--防止重複的table替換,造成死迴圈
local visited_sig = {}
function update_table(env_t, g_t, name, deep)
--對某些關鍵函式不進行比對
if protection[env_t] or protection[g_t] then return end
--如果原值與當前值記憶體一致,值一樣不進行對比
if env_t == g_t then return end
local signature = tostring(g_t)..tostring(env_t)
if visited_sig[signature] then return end
visited_sig[signature] = true
--遍歷對比值,如進行遍歷env類似的步驟
for name, value in pairs(env_t) do
local old_value = g_t[name]
if type(value) == type(old_value) then
if type(value) == 'function' then
update_func(value, old_value, name, deep..' '..name..' ')
g_t[name] = value
elseif type(value) == 'table' then
update_table(value, old_value, name, deep..' '..name..' ')
end
else
g_t[name] = value
end
end
--遍歷table的元表,進行對比
local old_meta = debug.getmetatable(g_t)
local new_meta = debug.getmetatable(env_t)
if type(old_meta) == 'table' and type(new_meta) == 'table' then
update_table(new_meta, old_meta, name..'s Meta', deep..' '..name..'s Meta'..' ' )
end
end
模擬的class的更新細節
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")
oldFunc = self.oldFunc
end
end
end
五、管理每一個Lua檔案的載入
為了每個要過載的Lua檔案,以model為名放到changeList的表中。
在 reload 前建立一個沙盒。讓 reload 過程不要溢位沙盒。一旦有這種情況至少呼叫者可以知道。
約束比較簡單,就是隻更新函式,不更新除函式以外的東西
可能會有的問題:
- 不用 upvaluejoin 是不能將 upvalue 關聯對的。只有 upvalue 是 table 且執行時不會修改 upvalue 才可以正確執行。
- 遍歷 VM 不周全。沒有遍歷 userdata ,沒有遍歷 thread 呼叫棧。針對 5.1 來說,還需要遍歷函式的 env 。
- 簡單遍歷 module table 是不能保證找到所有 module 相關的函式的。
六、關於熱更新涉及的點
- upvalue
- getupvalue (f, up), setupvalue (f, up, value)
- _G和debug.getregistry
- getfenv(object) ,setfenv(function,_ENV)
附
參考: