什么是熱更新,對(duì)于它的理解煎饼,正如云風(fēng)所說的那樣,熱更新更多的用途是做不停機(jī)的 bug 修復(fù)校赤,不應(yīng)用于常規(guī)的版本更新吆玖。對(duì)于熱更新的博客筒溃,網(wǎng)上看了不少,包括云風(fēng)寫的一篇 熱更文章沾乘。也仔細(xì)看了 snax 的熱更部分實(shí)現(xiàn)細(xì)節(jié)怜奖。發(fā)現(xiàn)有不少可以吸取之處。并把核心部分抽取出來翅阵,做個(gè)簡單分享歪玲。
至于怎么個(gè)熱更新法,更新的是哪些內(nèi)容掷匠,我的理解是滥崩,熱更新最好只更新模塊中的一小部分,比如其中的某個(gè)函數(shù)讹语,而不是將這個(gè)模塊都一起更新替換钙皮。盡量做到小改動(dòng),以達(dá)到最終目的顽决。至于更新的思路短条,我歸納為兩點(diǎn):
- 將模塊中舊的函數(shù)替換成新的函數(shù),這個(gè)新的函數(shù)可以放到一個(gè)lua文件中擎值,或者以字符串的形式給出慌烧。
- 將模塊中舊的函數(shù),當(dāng)前用到的所有上值鸠儿,(什么是上值屹蚊,后面有講到)保存到起來,用于新函數(shù)引用进每,保證新函數(shù)作為模塊中的一部分能夠正確運(yùn)行汹粤。
下面以一個(gè)demo為例,這也是我抽取 snax 模塊中熱更新部分田晚,以及和他人一起探討寫的嘱兼。
目錄結(jié)構(gòu):
./main.lua 調(diào)用 test.lua,做為運(yùn)行文件贤徒,顯示最終運(yùn)行效果
./test.lua 一個(gè)簡單模塊文件芹壕,用于提供熱更新的來源
./test_hot.lua 用于更新替換 test 模塊中的某些函數(shù),更新文件
./hotfix.lua 實(shí)現(xiàn)熱更新機(jī)制
通過這幅關(guān)系圖接奈,可以了解到踢涌,test 模塊和 test_hot 之間的關(guān)系,test_hot 負(fù)責(zé)更新 test 模塊中的某些函數(shù)序宦,但更新后的這些函數(shù)依然屬于 test 模塊中的一部分睁壁,并沒有脫離 test 模塊的掌控,而獨(dú)立出來。
現(xiàn)在我們看看 test.lua 包含了哪些內(nèi)容潘明,分別有 一個(gè)局部變量 index行剂,兩個(gè)函數(shù) print_index,show 钳降,函數(shù)體分別是圓圈1和2厚宰,兩個(gè)函數(shù)都引用到了這個(gè)局部變量 index。
假設(shè)當(dāng)前牲阁,我們想更新替換掉 print_index 函數(shù)固阁,讓其 index 加1 操作,并打印 index 值城菊,那么我們可以在 test_hot.lua 文件中這么寫备燃,見下圖黃色框部分:
我們希望在 print_index 更新后, index 加 1 后凌唬,show 函數(shù)獲取到的 index 值是 1并齐,即把更新函數(shù)也看作是 test.lua 模塊中的一部分。而不應(yīng)該是 index 加 1 后客税,show 函數(shù)獲取到的還是原值 0况褪。
假設(shè)我們希望更新 print_index 后,再一次更新更耻,把 index 值直接設(shè)置為 100测垛,那么它又應(yīng)該是這樣子的,見下圖最左側(cè)黃色部分:
通過這幾幅圖秧均,我們可以大致猜想到食侮,熱更新后,應(yīng)該是個(gè)什么效果目胡。
再談及熱更之前锯七,先要介紹幾過 lua 概念。一個(gè)是 _ENV 環(huán)境變量誉己,一個(gè)是上值 upvalue眉尸。
_ENV
在 lua 程序設(shè)計(jì)一書中有過這樣的解釋,lua 語言并沒有全局變量巨双,所謂的全局變量都是通過某種手段模擬出來的噪猾。
Lua 語言是在一個(gè)名為 _ENV 的預(yù)定義上值(一個(gè)外部的局部變量,upvalue)存在的情況下編譯所有的代碼段的筑累。因此畏妖,所有的變量要么綁定到一個(gè)名稱的局部變量,要么是 _ENV 中的一個(gè)字段疼阔,而 _ENV 本身是一個(gè)局部變量。
例如:
local z = 10
x = 0
y = 1
x = y + z
等價(jià)于
local z = 10
_ENV.x = 0
_ENV.y = 1
_ENV.x = _ENV.y + z
x,y 都是不用 local 聲明婆廊,z 是 local 聲明迅细。
所以,我們用到的全局變量其實(shí)是保存到 _ENV 變量中淘邻。lua 語言在內(nèi)部維護(hù)了一個(gè)表來作用全局環(huán)境(_G)茵典,通常,我們?cè)?load 一個(gè)代碼段宾舅,一個(gè)模塊時(shí)统阿,lua 會(huì)用這個(gè)表(_G)來初始化 _ENV。如果上面的幾行代碼是寫在一個(gè)文件中筹我,那么當(dāng) load 調(diào)用它時(shí)扶平,又會(huì)等價(jià)于:
-- xxx.lua 文件
local _ENV = the global environment (全局環(huán)境)
return function(...)
local z = 10
_ENV.x = 0
_ENV.y = 1
_ENV.x = _ENV.y +z
end
upvalue
當(dāng)一個(gè)局部變量被內(nèi)層的函數(shù)中使用的時(shí)候, 它被內(nèi)層函數(shù)稱作 上值蔬蕊,或是 外部局部變量结澄。引用 Lua 5.3 參考手冊(cè)
例如:
local x = 10
function hello(a, b)
local c = a + b + x
print(c)
end
那么在這段代碼中,hello 函數(shù)的上值有 變量 x岸夯,_ENV麻献,而我們剛剛講到,print 沒有經(jīng)過聲明猜扮,就可以直接使用勉吻,那么它肯定是保存于 _ENV 表中,print(c) 等價(jià)于 _ENV.print(c)旅赢,而變量 a齿桃、b、c 都是做為 hello 函數(shù)的局部變量鲜漩。
了解這個(gè)這個(gè)上值的概率源譬,我們才能在 hotfix 模塊中理解代碼的含義。下面就來看下具體 demo 的實(shí)現(xiàn)孕似。
-- main.lua
local hotfix = require "hotfix"
local test = require "test"
local test_hot = require "test_hot"
print("before hotfix")
for i = 1, 5 do
test.print_index() -- 熱更前踩娘,調(diào)用 print_index,打印 index 的值
end
hotfix.update(test.print_index, test_hot) -- 收集舊函數(shù)的上值喉祭,用于新函數(shù)的引用养渴,這個(gè)對(duì)應(yīng)之前說的歸納第2小點(diǎn)
test.print_index = test_hot -- 新函數(shù)替換舊的函數(shù),對(duì)應(yīng)之前說的歸納第1小點(diǎn)
print("after hotfix")
for i = 1, 5 do
test.print_index() -- 打印更新后的 index 值
end
test.show() -- show 函數(shù)沒有被熱更泛烙,但它獲取到的 index 值應(yīng)該是 最新的理卑,即 index = 5。
接下來看看 test.lua 模塊內(nèi)容
-- test.lua
local test = {}
local index = 0
function test.print_index()
print(index)
end
function test.show( )
print("show:", index)
end
return test
再看看 熱更文件 test_hot.lua 內(nèi)容
-- test_hot.lua
local index -- 這個(gè) index 必須聲明蔽氨,不用賦值藐唠,才能夠引用到 test 模塊中的局部變量 index
return function () -- 返回一個(gè)閉包函數(shù)帆疟,這個(gè)就是要更新替換后的原型
index = index + 1
print(index)
end
最后,再看看 hotfix.lua
-- hotfix.lua
local hotfix = {}
local function collect_uv(f, uv)
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then -- 當(dāng)所有上值收集完時(shí)宇立,跳出循環(huán)
break
end
if not uv[name] then
uv[name] = { func = f, index = i } -- 這里就會(huì)收集到舊函數(shù) print_index 所有的上值踪宠,包括變量 index
if type(value) == "function" then
collect_uv(value, uv)
end
end
i = i + 1
end
end
local function update_func(f, uv)
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then -- 當(dāng)所有上值收集完時(shí),跳出循環(huán)
break
end
-- value 值為空妈嘹,并且這個(gè) name 在 舊的函數(shù)中存在
if not value and uv[name] then
local desc = uv[name]
-- 將新函數(shù) f 的第 i 個(gè)上值引用舊模塊 func 的第 index 個(gè)上值
debug.upvaluejoin(f, i, desc.func, desc.index)
end
-- 只對(duì) function 類型進(jìn)行遞歸更新柳琢,對(duì)基本數(shù)據(jù)類型(number、boolean润脸、string) 不管
if type(value) == "function" then
update_func(value, uv)
end
i = i + 1
end
end
function hotfix.update(old, new)
local uv = {}
collect_uv(old, uv)
update_func(new, uv)
end
return hotfix
這個(gè)用到了 lua 的兩個(gè) api 函數(shù)柬脸,在 Lua 5.3 參考手冊(cè) 中有介紹。
debug.getupvalue (f, up)
此函數(shù)返回函數(shù)f
的第up
個(gè)上值的名字和值毙驯。 如果該函數(shù)沒有那個(gè)上值倒堕,返回 nil 。
debug.upvaluejoin (f1, n1, f2, n2)
讓 Lua 閉包f1
的第n1
個(gè)上值 引用 Lua 閉包f2
的第n2
個(gè)上值尔苦。
我們可以看到涩馆, hotfix.lua 做的事也是比較簡單的,主要是收集 舊函數(shù)的所有上值允坚,更新到新函數(shù)中魂那。最后一步替換舊函數(shù)是在 main.lua 中完成。
最后看看運(yùn)行結(jié)果:
[root@instance test]# lua main.lua
before hotfix
0
0
0
0
0
after hotfix
1
2
3
4
5
-------------
show: 5
在了解了熱更新機(jī)制后稠项,最后來思考幾個(gè)問題
- 在熱更文件 test_hot.lua 中涯雅,如果要更新的函數(shù)有很多,那么要聲明的變量就會(huì)有很多展运,這個(gè)繁瑣的事情活逆,應(yīng)該如何解決。
- 如果要更新的是 test.lua 中的局部函數(shù)拗胜,而這個(gè)局部函數(shù)又同時(shí)被多個(gè)其他函數(shù)引用到蔗候,改怎么熱更,才能解決其他函數(shù)引用問題埂软。
- hotfix.lua 中的 collect_uv 函數(shù)锈遥,目前只對(duì)上值是 function 類型,才繼續(xù)遞歸收集上值勘畔。就有可能會(huì)有一些上值沒辦法繼續(xù)收集到所灸,比如表,在 test.lua 中加入如下內(nèi)容炫七,那么 cmd 中的 show 方法爬立,就沒辦法收集到。
...
local cmd = {}
function cmd.show()
end
function test.getcmd(name)
local c = cmd[name]
if c then c() and
end