1. 程式人生 > 其它 >Lua熱過載

Lua熱過載

技術標籤:Lua工具Lua

之前專案用Lua的模組很少,確實沒關注是否在客戶端部分實現熱過載。因為專案的伺服器開發是C++和Lua的組合,在配合處理開發的時候,伺服器指令碼實現熱過載。在客戶端使用Lua的模組越來越多,也有人更多的同事開始用Lua開發。為了提高開發效率,覺得還是可以花點時間在客戶端實現下Lua熱過載。

Lua的特點:基於暫存器的虛擬機器,簡潔的語法,高效的編譯執行,容易嵌入的特性。Lua在國內網際網路技術上的應用也佔領不少市場,redis,openresty, skynet等等都能看到Lua忙碌的身影。

一、原理

函式requier在表中package.loaded中檢查模組是否已被載入。

最簡單粗暴的熱更新就是將package.loaded[modelname]的值置為nil,強制重新載入:

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 過程不要溢位沙盒。一旦有這種情況至少呼叫者可以知道。

約束比較簡單,就是隻更新函式,不更新除函式以外的東西

可能會有的問題:

  1. 不用 upvaluejoin 是不能將 upvalue 關聯對的。只有 upvalue 是 table 且執行時不會修改 upvalue 才可以正確執行。
  2. 遍歷 VM 不周全。沒有遍歷 userdata ,沒有遍歷 thread 呼叫棧。針對 5.1 來說,還需要遍歷函式的 env 。
  3. 簡單遍歷 module table 是不能保證找到所有 module 相關的函式的。

詳細程式碼

作者相應的部落格文章【Lua熱更新原理】

六、關於熱更新涉及的點

  • upvalue
  • getupvalue (f, up), setupvalue (f, up, value)
  • _G和debug.getregistry
  • getfenv(object) ,setfenv(function,_ENV)

參考:

1.cloudwu/luareload

2.如何讓 lua 做盡量正確的熱更新

3.【reload script】lua客戶端指令碼熱更

4.Lua指令碼熱更新

5.Lua-熱更新小結