和lua的效率对比测试_Unity游戏开发Lua更新运行时代码!
一、用來卸載表格的加載
最簡單粗暴的熱更新就是將package.loaded[modelname]的值置為nil,強制重新加載:
function reload_module_obsolete(module_name)package.loaded[module_name] = nil
require(module_name)
end
這樣做雖然能完成熱更,但問題是已經引用了該模塊的地方不會得到更新, 因此我們需要將引用該模塊的地方的值也做對應的更新。
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
二、我認為邏輯最清晰的,但是可能是我不會用吧!
源鏈接:鏈接
--region 利用_ENV環境,在加載的時候把數據加載到_ENV下,然后再通過對比的方式修改_G底下的值,從而實現熱更新,函數 -- 失敗function ReloadUtil.hotfix(chunk, check_name)
check_name = check_name or 'hotfix'
local env = {}
setmetatable(env, { __index = _G })
local f, err = load(chunk, check_name, 't', env)
assert(f,err)
local ok, err = pcall(f)
assert(ok,err)
local protection = {
setmetatable = true,
pairs = true,
ipairs = true,
next = true,
require = true,
_ENV = true,
}
--防止重復的table替換,造成死循環
local visited_sig = {}
function ReloadUtil.update_table(newTable, oldTable, name, deep)
--對某些關鍵函數不進行比對
if protection[newTable] or protection[oldTable] then return end
--如果原值與當前值內存一致,值一樣不進行對比
if newTable == oldTable then return end
local signature = tostring(oldTable)..tostring(newTable)
if visited_sig[signature] then return end
visited_sig[signature] = true
--遍歷對比值,如進行遍歷env類似的步驟
for name, newValue in pairs(newTable) do
local old_value = oldTable[name]
if type(newValue) == type(old_value) then
if type(newValue) == 'function' then
ReloadUtil.update_func(newValue, old_value, name, deep..' '..name..' ')
oldTable[name] = newValue
elseif type(newValue) == 'table' then
ReloadUtil.update_table(newValue, old_value, name, deep..' '..name..' ')
end
else
oldTable[name] = newValue
end
end
--遍歷table的元表,進行對比
local old_meta = debug.getmetatable(oldTable)
local new_meta = debug.getmetatable(newTable)
if type(old_meta) == 'table' and type(new_meta) == 'table' then
ReloadUtil.update_table(new_meta, old_meta, name..'s Meta', deep..' '..name..'s Meta'..' ' )
end
end
function ReloadUtil.update_func(newFunc, oldFunc, name, deep)
--取得原值所有的upvalue,保存起來
local old_upvalue_map = {}
for i = 1, math.huge do
local name, value = debug.getupvalue(oldFunc, 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(newFunc, 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(newFunc, i, old_value)
elseif type(old_value) == 'function' then
ReloadUtil.update_func(value, old_value, name, deep..' '..name..' ')
elseif type(old_value) == 'table' then
ReloadUtil.update_table(value, old_value, name, deep..' '..name..' ')
debug.setupvalue(newFunc, i, old_value)
else
debug.setupvalue(newFunc, i, old_value)
end
end
end
end
--原理
--利用_ENV環境,在加載的時候把數據加載到_ENV下,然后再通過對比的方式修改_G底下的值,從而實現熱更新,函數
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
ReloadUtil.update_func(value, g_value, name, 'G'..' ')
_G[name] = value
elseif type(value) == 'table' then
ReloadUtil.update_table(value, g_value, name, 'G'..' ')
end
end
return 0
end
function ReloadUtil.hotfix_file(debugName)
local newCode
local fp = io.open(debugName)
if fp then
newCode = fp:read('*all')
io.close(fp)
end
if not newCode then
return -1
end
return ReloadUtil.hotfix(newCode, debugName)
end
--endregion
圖文無關
三、有點復雜,遞歸了debug.getregistry(),然后去替換舊的值
源鏈接:asqbtcupid.github.io/lu?有介紹原理,講得還蠻細的,學習了蠻多,但是這個遞歸真的是復雜,我注釋掉了一些遞歸,能滿足基本的需求。
local ReloadUtil = {}local tableInsert = table.insert
local tableRemove = table.remove
local tableConcat = table.concat
local ioPopen = io.popen
local ioInput = io.input
local ioRead = io.read
local stringMatch = string.match
local stringFind = string.find
local stringSub = string.sub
local stringGsub = string.gsub
local packageLoaded = package.loaded
local type = type
local getfenv = getfenv
local setfenv = setfenv
local loadstring = loadstring
local mathHuge = math.huge
local debugGetupvalue = debug.getupvalue
local debugSetupvalue = debug.setupvalue
local debugGetmetatable = debug.getmetatable
local debugSetfenv = debug.setfenv
function ReloadUtil.FailNotify(...)
printAError(...)
end
function ReloadUtil.DebugNofity(...)
print(...)
end
function ReloadUtil.ErrorHandle(e)
ReloadUtil.FailNotify("HotUpdate Error\n"..tostring(e))
ReloadUtil.ErrorHappen = true
end
function ReloadUtil.InitProtection()
ReloadUtil.Protection = {}
local Protection = ReloadUtil.Protection
Protection[setmetatable] = true
Protection[pairs] = true
Protection[ipairs] = true
Protection[next] = true
Protection[require] = true
Protection[ReloadUtil] = true
Protection[ReloadUtil.Meta] = true
Protection[math] = true
Protection[string] = true
Protection[table] = true
end
local function Normalize(path)
path = path:gsub("/","\\")
local pathLen = #path
if path:sub(pathLen, pathLen) == "\\" then
path = path:sub(1, pathLen - 1)
end
local parts = { }
for w in path:gmatch("[^\\]+") do
if w == ".." and #parts ~=0 then tableRemove(parts)
elseif w ~= "." then tableInsert(parts, w)
end
end
return tableConcat(parts, "\\")
end
-- 根據給的路徑,找到路徑下所有文件,HU.FileMap[FileName] = {SysPath = line, LuaPath = luapath}
function ReloadUtil.InitFileMap(RootPath)
local systemPathList = {}
local HotUpdateDic = ReloadUtil.HotUpdateDic
local OldCode = ReloadUtil.OldCode
RootPath = Normalize(RootPath)
--獲取一個File對象其下的所有文件和目錄的絕對路徑: 的所有文件(/S/B),不包括文件夾(/A:A),ioPopen返回文件句柄file handle
--todo 這里有的問題,多次啟動后會報bad file decorator ,檢查是否是打開文件沒有關閉導致
local file = ioPopen("dir /S/B /A:A \""..RootPath.."\"")
for SysPath in ioInput(file):lines() do
local FileName = stringMatch(SysPath,".*\\(.*)%.lua")
--todo meta 優化一下regex
local metaFile = stringMatch(SysPath,".*\\(.*)%.lua.meta")
if FileName ~= nil and metaFile == nil then
--todo !!! luaPath在保存的時候是按文件夾路徑保存的比如:game.modules.XXX.lua,所以可能要自己搞對這個路徑
local startIndex = stringFind(SysPath,"game")
local luaPath = stringSub(SysPath, startIndex, #SysPath -4)
luaPath = stringGsub(luaPath, "\\", ".")
HotUpdateDic[luaPath] = SysPath
tableInsert(systemPathList,SysPath)
-- 初始化舊代碼
ioInput(SysPath)
OldCode[SysPath] = ioRead("*all")
ioInput():close()
end
end
file:close()
return systemPathList
end
function ReloadUtil.InitFakeTable()
local meta = {}
ReloadUtil.Meta = meta
local function FakeT() return setmetatable({}, meta) end
local function EmptyFunc() end
local function pairs() return EmptyFunc end
local function setmetatable(t, metaT)
ReloadUtil.MetaMap[t] = metaT
return t
end
local function getmetatable(t, metaT)
return setmetatable({}, t)
end
local function require(LuaPath)
if not ReloadUtil.RequireMap[LuaPath] then
local FakeTable = FakeT()
ReloadUtil.RequireMap[LuaPath] = FakeTable
end
return ReloadUtil.RequireMap[LuaPath]
end
function meta.__index(table, key)
if key == "setmetatable" then
return setmetatable
elseif key == "pairs" or key == "ipairs" then
return pairs
elseif key == "next" then
return EmptyFunc
elseif key == "require" then
return require
else
local FakeTable = FakeT()
rawset(table, key, FakeTable)
return FakeTable
end
end
function meta.__newindex(table, key, value) rawset(table, key, value) end
function meta.__call() return FakeT(), FakeT(), FakeT() end
function meta.__add() return meta.__call() end
function meta.__sub() return meta.__call() end
function meta.__mul() return meta.__call() end
function meta.__div() return meta.__call() end
function meta.__mod() return meta.__call() end
function meta.__pow() return meta.__call() end
function meta.__unm() return meta.__call() end
function meta.__concat() return meta.__call() end
function meta.__eq() return meta.__call() end
function meta.__lt() return meta.__call() end
function meta.__le() return meta.__call() end
function meta.__len() return meta.__call() end
return FakeT
end
function ReloadUtil.IsNewCode(SysPath)
ioInput(SysPath)
local newCode = ioRead("*all")
local oldCode = ReloadUtil.OldCode[SysPath]
if oldCode == newCode then
ioInput():close()
return false
end
ReloadUtil.DebugNofity(SysPath)
return true, newCode
end
function ReloadUtil.GetNewObject(newCode, LuaPath, SysPath)
--loadstring 一段lua代碼以后,會經過語法解析返回一個函數,執行返回的函數時,字符串中的代碼就被執行了。
local NewFunction = loadstring(newCode)
if not NewFunction then
ReloadUtil.FailNotify(SysPath.." has syntax error.")
collectgarbage("collect")
else
-- 把加載的字符串放置在空環境了,防止報錯
setfenv(NewFunction, ReloadUtil.FakeENV)
local NewObject
ReloadUtil.ErrorHappen = false
--類似其它語言里的 try-catch, xpcall 類似 pcall xpcall接受兩個參數:調用函數、錯誤處理函數
-- todo 父類沒拿到
xpcall(function () NewObject = NewFunction() end, ReloadUtil.ErrorHandle)
if not ReloadUtil.ErrorHappen then
ReloadUtil.OldCode[SysPath] = newCode
return NewObject
else
collectgarbage("collect")
end
end
end
function ReloadUtil.ResetENV(object, name, From, Deepth)
local visited = {}
local function f(object, name)
if not object or visited[object] then return end
visited[object] = true
if type(object) == "function" then
ReloadUtil.DebugNofity(Deepth.."HU.ResetENV", name, " from:"..From)
xpcall(function () setfenv(object, ReloadUtil.ENV) end, ReloadUtil.FailNotify)
elseif type(object) == "table" then
ReloadUtil.DebugNofity(Deepth.."HU.ResetENV", name, " from:"..From)
for k, v in pairs(object) do
f(k, tostring(k).."__key", " HU.ResetENV ", Deepth.." " )
f(v, tostring(k), " HU.ResetENV ", Deepth.." ")
end
end
end
f(object, name)
end
-- 遍歷_G這張全局表,替換HU.ChangedFuncList 有改動列表 的函數
function ReloadUtil.Travel_G()
local visited = {}
local ChangedFuncList = ReloadUtil.ChangedFuncList
visited[ReloadUtil] = true
local function f(table)
if (type(table) ~= "function" and type(table) ~= "table") or visited[table] or ReloadUtil.Protection[table] then return end
visited[table] = true
if type(table) == "function" then
for i = 1, mathHuge do
local name, value = debugGetupvalue(table, i)
if not name then break end
if type(value) == "function" then
for _, funcs in ipairs(ChangedFuncList) do
if value == funcs.OldObject then
debugSetupvalue(table, i, funcs.NewObject)
end
end
end
-- todo
--f(value)
end
elseif type(table) == "table" then
-- 不要漏掉元表和upvalue的表,元表的獲取用debug.getmetatable,
-- todo 這樣對于有metatable這個key的元表,也能正確獲取。
--f(debugGetmetatable(table))
local changeIndexList = {}
for key, value in pairs(table) do
-- todo 還有注意table的key也可以是函數。
--f(key)
f(value)
if type(value) == "function" then
for _, funcs in ipairs(ChangedFuncList) do
if value == funcs.OldObject then
table[key] = funcs.NewObject
end
end
end
-- 找出改動的index
if type(key) == "function" then
for index, funcs in ipairs(ChangedFuncList) do
if key == funcs.OldObject then
changeIndexList[#changeIndexList + 1] = index
end
end
end
end
-- 修改改動的值
for _, index in ipairs(changeIndexList) do
local funcs = ChangedFuncList[index]
table[funcs.NewObject] = table[funcs.OldObject]
table[funcs.OldObject] = nil
end
end
end
--遍歷_G這張全局表,_G在registryTable里
--f(_G)
--如果有宿主語言,那么還要遍歷一下注冊表,用debug.getregistry()獲得。
local registryTable = debug.getregistry()
f(registryTable)
end
function ReloadUtil.UpdateTable(OldTable, NewTable, Name, From, Deepth)
if ReloadUtil.Protection[OldTable] or ReloadUtil.Protection[NewTable] then return end
if OldTable == NewTable then return end
local signature = tostring(OldTable)..tostring(NewTable)
if ReloadUtil.VisitedSig[signature] then return end
ReloadUtil.VisitedSig[signature] = true
ReloadUtil.DebugNofity(Deepth.."HU.UpdateTable "..Name.." from:"..From)
for ElementName, newValue in pairs(NewTable) do
local OldElement = OldTable[ElementName]
if type(newValue) == type(OldElement) then
if type(newValue) == "function" then
ReloadUtil.UpdateOneFunction(OldElement, newValue, ElementName, OldTable, "HU.UpdateTable", Deepth.." ")
elseif type(newValue) == "table" then
ReloadUtil.UpdateTable(OldElement, newValue, ElementName, "HU.UpdateTable", Deepth.." ")
end
elseif OldElement == nil and type(newValue) == "function" then
-- 新增的函數,添加到舊環境里
if pcall(setfenv, newValue, ReloadUtil.ENV) then
OldTable[ElementName] = newValue
end
end
end
-- todo 更新metatable
--local OldMeta = debug.getmetatable(OldTable)
--local NewMeta = ReloadUtil.MetaMap[NewTable]
--if type(OldMeta) == "table" and type(NewMeta) == "table" then
-- ReloadUtil.UpdateTable(OldMeta, NewMeta, Name.."'s Meta", "HU.UpdateTable", Deepth.." ")
--end
end
-- Upvalue 是指那些函數外被引用到的local變量
function ReloadUtil.UpdateUpvalue(OldFunction, NewFunction, Name, From, Deepth)
ReloadUtil.DebugNofity(Deepth.."HU.UpdateUpvalue", Name, " from:"..From)
local OldUpvalueMap = {}
local OldExistName = {}
-- 記錄舊的upvalue表
for i = 1, mathHuge do
local name, value = debugGetupvalue(OldFunction, i)
if not name then break end
OldUpvalueMap[name] = value
OldExistName[name] = true
end
-- 新的upvalue表進行替換
for i = 1, mathHuge do
local name, value = debugGetupvalue(NewFunction, i)
if not name then break end
if OldExistName[name] then
local OldValue = OldUpvalueMap[name]
if type(OldValue) ~= type(value) then
-- 新的upvalue類型不一致時,用舊的upvalue
debugSetupvalue(NewFunction, i, OldValue)
elseif type(OldValue) == "function" then
-- 替換單個函數
ReloadUtil.UpdateOneFunction(OldValue, value, name, nil, "HU.UpdateUpvalue", Deepth.." ")
elseif type(OldValue) == "table" then
-- 對table里面的函數繼續遞歸替換
ReloadUtil.UpdateTable(OldValue, value, name, "HU.UpdateUpvalue", Deepth.." ")
debugSetupvalue(NewFunction, i, OldValue)
else
-- 其他類型數據有改變,也要用舊的
debugSetupvalue(NewFunction, i, OldValue)
end
else
-- 對新添加的upvalue設置正確的環境表
ReloadUtil.ResetENV(value, name, "HU.UpdateUpvalue", Deepth.." ")
end
end
end
function ReloadUtil.UpdateOneFunction(OldObject, NewObject, FuncName, OldTable, From, Deepth)
if ReloadUtil.Protection[OldObject] or ReloadUtil.Protection[NewObject] then return end
if OldObject == NewObject then return end
local signature = tostring(OldObject)..tostring(NewObject)
if ReloadUtil.VisitedSig[signature] then return end
ReloadUtil.DebugNofity(Deepth.."HU.UpdateOneFunction "..FuncName.." from:"..From)
ReloadUtil.VisitedSig[signature] = true
--最后注意把熱更新的函數的環境表再改回舊函數的環境表即可,方法是setfenv(newfunction, getfenv(oldfunction))。
if pcall(debugSetfenv, NewObject, getfenv(OldObject)) then
ReloadUtil.UpdateUpvalue(OldObject, NewObject, FuncName, "HU.UpdateOneFunction", Deepth.." ")
ReloadUtil.ChangedFuncList[#ReloadUtil.ChangedFuncList + 1] = {OldObject = OldObject,NewObject = NewObject,FuncName = FuncName,OldTable = OldTable}
end
end
function ReloadUtil.ReplaceOld(OldObject, NewObject, LuaPath, From)
if type(OldObject) == type(NewObject) then
if type(NewObject) == "table" then
ReloadUtil.UpdateTable(OldObject, NewObject, LuaPath, From, "")
elseif type(NewObject) == "function" then
ReloadUtil.UpdateOneFunction(OldObject, NewObject, LuaPath, nil, From, "")
end
end
end
function ReloadUtil.HotUpdateCode(LuaPath, SysPath)
local OldObject = packageLoaded[LuaPath]
if not OldObject then
-- 沒加載的就不熱更??
return
end
local isNew,newCode = ReloadUtil.IsNewCode(SysPath)
if not isNew then
return
end
local newObject = ReloadUtil.GetNewObject(newCode,SysPath,LuaPath)
-- 更新舊代碼
ReloadUtil.ReplaceOld(OldObject, newObject, LuaPath, "Main")
--原理
--利用_ENV環境,在加載的時候把數據加載到_ENV下,然后再通過對比的方式修改_G底下的值,從而實現熱更新,函數
--setmetatable(ReloadUtil.FakeENV, nil)
--todo ??
--ReloadUtil.UpdateTable(ReloadUtil.ENV, ReloadUtil.FakeENV, " ENV ", "Main", "")
-- 替換完,上一次的代碼就是舊代碼
ReloadUtil.OldCode[SysPath] = newCode
end
-- 外部調用(先Init需要的路徑,然后Update是熱更時候調用的)
---@param RootPath 需要被更新的文件夾路徑
---@param UpdateListFile 需要被更新的文件列表,不傳為整個文件夾,會卡
function ReloadUtil.Init(RootPath, ENV)
ReloadUtil.HotUpdateDic = {}
ReloadUtil.FileMap = {}
ReloadUtil.OldCode = {}
ReloadUtil.ChangedFuncList = {}
ReloadUtil.VisitedSig = {}
ReloadUtil.FakeENV = ReloadUtil.InitFakeTable()()
--當我們加載lua模塊的時候,這時候這個模塊信息并不像初始化全局代碼一樣,就算提前設置了package.loaded["AA"] = nil,
--也不會出現在env中同時也不會調用_G的__newindex函數,也就是說env["AA"]為空,故這種寫法無法進行熱更新,所以通常模塊的寫法改成如下
--定義模塊AA
local AA = {}
--相當于package.seeall
setmetatable(AA, {__index = _G})
--環境隔離
local _ENV = AA
ReloadUtil.NewENV = _ENV
ReloadUtil.ENV = ENV or _G
ReloadUtil.InitProtection()
ReloadUtil.ALL = false
return ReloadUtil.InitFileMap(RootPath)
end
function ReloadUtil.Update()
ReloadUtil.VisitedSig = {}
ReloadUtil.ChangedFuncList = {}
for LuaPath, SysPath in pairs(ReloadUtil.HotUpdateDic) do
ReloadUtil.HotUpdateCode(LuaPath, SysPath)
end
if #ReloadUtil.ChangedFuncList > 0 then
ReloadUtil.Travel_G()
end
collectgarbage("collect")
end
我跟老大炫耀的時候,老大說,那你懂其中的原理嗎,一下問懵我了,老大說,你要學習到其中的原理才能進步啊,不然就只是個會用工具的人。好有道理,搞得我羞愧難當,趕緊好好學習其中原理。
Upvalue
Upvalue 是指那些函數外被引用到的local變量,比如:local a = 1function foo()
print(a)
end那么a就是這個foo的upvalue。
getupvalue (f, up)
此函數返回函數 f 的第 up 個上值的名字和值。如果該函數沒有那個上值,返回 nil 。以 '(' (開括號)打頭的變量名表示沒有名字的變量 (去除了調試信息的代碼塊)。
setupvalue (f, up, value):
這個函數將 value 設為函數 f 的第 up 個上值。如果函數沒有那個上值,返回 nil 否則,返回該上值的名字。_G和debug.getregistry這2張表
學習的時候,我一直以為只有把_G這張全局表的舊值替換掉就好了,然后真正實施的時候,還是會有種種問題,實在是很糟糕,看這段代碼的時候一直不是很理解,看了debug.getregistry的定義:debug.getregistry():返回注冊表表,這是一個預定義出來的表, 可以用來保存任何 C 代碼想保存的 Lua 值。還是半桶水,但是我有注意到_G這種表其實在debug.getregistry返回的這張注冊表里有,所以最后就遞歸這張表其替換里面的舊值就好了。
getfenv(object):返回對象的環境變量。
setfenv(function,_ENV):設置一段代碼的運行環境
io.popen("dir /S/B /A:A \""..RootPath.."\""):獲取一個File對象其下的所有文件和目錄的絕對路徑: 的所有文件(/S/B),不包括文件夾(/A:A),io.popen返回文件句柄file handle
對了,InitFileMap這個函數有個要注意的,luaPath在保存的時候是按文件夾路徑保存的比如:game.modules.XXX.lua,所以可能要自己搞對這個路徑;
最后總結一下流程:
1. 初始化需要熱更的文件路徑,用一張哈希表存下來;
2. 然后遍歷這些路徑,讀取所有的代碼,判斷是否是新代碼,新的話記錄到ChangedFuncList列表里面;3.?新代碼的話,就把加載的字符串放置在空環境了,防止報錯setfenv(NewFunction, ReloadUtil.FakeENV);4.?代替舊代碼,拿著ChangedFuncList列表到注冊表(debug.getregistry())里面去找舊值,遞歸注冊表的舊值替換成新的值。
來源知乎專欄:Unity手游開發叨叨
總結
以上是生活随笔為你收集整理的和lua的效率对比测试_Unity游戏开发Lua更新运行时代码!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 国债价格和利率的关系,两点很重要
- 下一篇: UE4 查看打包文件内容