Lua熱更新實(shí)現(xiàn)
用途
在生產(chǎn)環(huán)境上锰镀,總有可能出現(xiàn)不可預(yù)知的Bug斟赚,而通常修改好Bug僅僅又修改幾句着降,停機(jī)維護(hù)的成本又太高,對(duì)于游戲來(lái)說(shuō)拗军,通常每個(gè)服就是單獨(dú)的進(jìn)程任洞,也做不到像分布式環(huán)境下,關(guān)掉一部分機(jī)器发侵,先升級(jí)一部分交掏,再升級(jí)另一部分的無(wú)縫升級(jí)。這時(shí)候如果有熱更就可以迅速的把Bug修復(fù)方案通過(guò)熱更新進(jìn)行修復(fù)刃鳄,不會(huì)對(duì)用戶任何的影響盅弛。例如:
- 業(yè)務(wù)邏輯有Bug
- 配置的數(shù)據(jù)有誤
- 需求發(fā)生變更
熱更新的原則
1、熱更新不破壞原有數(shù)據(jù)
熱更新更新的基本內(nèi)容就是更新服務(wù)的邏輯,通常只是邏輯發(fā)生變化挪鹏,但原有的值并不能被改變见秽,例如:
local a = 1
function get_a()
return a
end
此時(shí),我們調(diào)用get_a()返回是的1讨盒,我們將熱更成
local a = 2
function get_a()
print("get_a function")
return a
end
此時(shí)我們改變了a的初始值解取,但我們并不知道之前服務(wù)a的值是不是被重新賦過(guò)值,假設(shè)熱更前a的值仍然為1返顺,那么我們熱更后調(diào)用get_a()返回的應(yīng)該是1禀苦,而不應(yīng)受新的初始值影響,而且同能打印出了"get_a function"遂鹊,這時(shí)候則認(rèn)為熱更正常振乏。
2、不為熱更新寫更多的代碼
熱更新可以通過(guò)很多種方法實(shí)現(xiàn)秉扑,比如說(shuō)模塊為了支持?jǐn)?shù)據(jù)不變的特性慧邮,需要在模塊里額外寫一些代碼來(lái)記錄舊值,熱更新之后再把舊值copy過(guò)來(lái)邻储,或者用一些特殊的語(yǔ)法來(lái)支撐赋咽。這種方法將會(huì)對(duì)項(xiàng)目增加很多的負(fù)擔(dān)旧噪,而且一旦發(fā)生意料之外的Bug吨娜,熱更系統(tǒng)幾乎處于半癱瘓狀態(tài)。應(yīng)該來(lái)說(shuō)淘钟,代碼原本該怎么實(shí)現(xiàn)就怎么實(shí)現(xiàn)宦赠,對(duì)于99%的lua代碼都是支持的,不需要修改來(lái)迎合熱更新米母。通常熱更新不改變?cè)凶兞恐档念愋汀?/p>
熱更新的實(shí)現(xiàn)勾扭,代碼適用于5.2以上
原理
利用_ENV環(huán)境,在加載的時(shí)候把數(shù)據(jù)加載到_ENV下铁瞒,然后再通過(guò)對(duì)比的方式修改_G底下的值妙色,從而實(shí)現(xiàn)熱更新,函數(shù)
function hotfix(chunk, check_name)
定義env的table慧耍,并為env設(shè)置_G訪問(wèn)權(quán)限身辨,然后調(diào)用load實(shí)現(xiàn)把數(shù)據(jù)重新加載進(jìn)來(lái)
local env = {}
setmetatable(env, { __index = _G })
local _ENV = env
local f, err = load(chunk, check_name, 't', env)
assert(f,err)
local ok, err = pcall(f)
assert(ok,err)
此時(shí)env我們可以得到新函數(shù)有變更的部分,我們替換的為可見變量芍碧,也就是可直接訪問(wèn)的變量
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
通過(guò)env當(dāng)前的值和_G當(dāng)前的值進(jìn)行對(duì)比
- 如果類型不同我們直接覆蓋原值煌珊,此時(shí)value不為nil,不會(huì)出現(xiàn)原則被覆蓋成nil的情況
- 如果當(dāng)前值為函數(shù)泌豆,我們進(jìn)行函數(shù)的upvalue值比對(duì)
function update_func(env_f, g_f, name, deep)
--取得原值所有的upvalue定庵,保存起來(lái)
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,根據(jù)名字和原值對(duì)比,如果原值不存在則進(jìn)行跳過(guò)蔬浙,如果為其它值則進(jìn)行遍歷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
- 如果當(dāng)前值為table猪落,我們遍歷table值進(jìn)行對(duì)比
local protection = {
setmetatable = true,
pairs = true,
ipairs = true,
next = true,
require = true,
_ENV = true,
}
--防止重復(fù)的table替換,造成死循環(huán)
local visited_sig = {}
function update_table(env_t, g_t, name, deep)
--對(duì)某些關(guān)鍵函數(shù)不進(jìn)行比對(duì)
if protection[env_t] or protection[g_t] then return end
--如果原值與當(dāng)前值內(nèi)存一致畴博,值一樣不進(jìn)行對(duì)比
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
--遍歷對(duì)比值许布,如進(jìn)行遍歷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的元表,進(jìn)行對(duì)比
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
更新
1绎晃、可以調(diào)用hotfix_file對(duì)整個(gè)文件進(jìn)行熱更
function hotfix_file(name)
local file_str
local fp = io.open(name)
if fp then
io.input(name)
file_str = io.read('*all')
io.close(fp)
end
if not file_str then
return -1
end
return hotfix(file_str, name)
end
2蜜唾、可以通過(guò)hotfix進(jìn)行代碼的更新
function hotfix(chunk, check_name)
關(guān)于坑
這里有一個(gè)注意事項(xiàng),lua的module模塊庶艾,如:
module("AA", package.seeall)
當(dāng)我們加載lua模塊的時(shí)候袁余,這時(shí)候這個(gè)模塊信息并不像初始化全局代碼一樣,就算提前設(shè)置了package.loaded["AA"] = nil, 也不會(huì)出現(xiàn)在env中同時(shí)也不會(huì)調(diào)用_G的__newindex函數(shù)咱揍,也就是說(shuō)env["AA"]為空颖榜,故這種寫法無(wú)法進(jìn)行熱更新,所以通常模塊的寫法改成如下
--定義模塊AA
AA = {}
--相當(dāng)于package.seeall
setmetatable(AA, {__index = _G})
--環(huán)境隔離
local _ENV = AA