InjectFix是騰訊最新對(duì)外開(kāi)源的Unity代碼邏輯熱修復(fù)方案,可實(shí)現(xiàn)在Unity線上客戶端內(nèi),不用迭代新版本,就能快速修復(fù)游戲的線上bug。
先說(shuō)幾個(gè)亮點(diǎn):
InjectFix經(jīng)騰訊內(nèi)部多個(gè)項(xiàng)目應(yīng)用反饋十分良好,不僅能解決線上bug,還可以有效的提高日常開(kāi)發(fā)效率,下面我們聊下這項(xiàng)目的前世今生。
熱更方案大亂斗
所有支持iOS的熱更方案都有個(gè)共同點(diǎn):更新后代碼都是解析執(zhí)行。如果按其更新前是否解析執(zhí)行,可以分為兩大類:
一類是某些模塊甚至整個(gè)游戲,都一直解析執(zhí)行。這是最傳統(tǒng)的方式,目前市面上所有主流方案(xLua,slua,tolua,ILRuntime,jsb等等)都支持這種方式。這種方式的特點(diǎn):
ps:也有一種思路是通過(guò)一個(gè)C#轉(zhuǎn)XX腳本工具來(lái)實(shí)現(xiàn)C#編碼,解析執(zhí)行,但如果你是一個(gè)已有項(xiàng)目想這么轉(zhuǎn)一下,大概率是失敗的,除非你一開(kāi)始就在用這方式在開(kāi)發(fā),碰到坑就避開(kāi),因?yàn)檫@類方案往往不是完整支持全部語(yǔ)法,支持的語(yǔ)法也不一定能完全一致。
另外一類是以原生方式跑,如果有bug,把邏輯重定向到新的,解析執(zhí)行的邏輯。這種方式的特點(diǎn):
第二種方式是接下來(lái)討論的重點(diǎn),方便起見(jiàn),我們稱之為“熱修復(fù)”,熱修復(fù)最早的成熟方案是xLua提供,經(jīng)過(guò)兩年來(lái)的使用已經(jīng)逐漸被接受,tolua#后來(lái)也加入了這功能,也有一些網(wǎng)友基于ILRuntime做了熱修復(fù)功能。
InjectFix是什么?
InjectFix就是一個(gè)熱修復(fù)的實(shí)現(xiàn)。那它和其它熱修復(fù)方案又有什么不同呢?
設(shè)想這么個(gè)場(chǎng)景,我們有一個(gè)一千行代碼的函數(shù),其中有一行有問(wèn)題,我們需要修復(fù)它。
如果用xLua,需要用lua去重新實(shí)現(xiàn)一遍這個(gè)函數(shù),工作量大。而基于ILRuntime的熱修復(fù),由于其補(bǔ)丁是另一個(gè)程序集,它無(wú)法直接訪問(wèn)原類的私有成員,所以那999行正常代碼一般也不能直接使用,需要做較多修改。
而InjectFix不需要用lua,也不需要像ILRuntime熱修復(fù)那樣另外建一個(gè)工程把那一千行邏輯重實(shí)現(xiàn)。只需要在Unity原工程直接改掉這行代碼,然后標(biāo)注這函數(shù)要更新即可。
不僅如此,InjectFix還有其它優(yōu)勢(shì):
- 運(yùn)行時(shí)非常小巧,僅100K左右,比各lua方案,ILRuntime都要小很多,而且不依賴第三方庫(kù),純C#實(shí)現(xiàn)。
- 支持每個(gè)游戲生成一份自己私有的補(bǔ)丁格式,私有的指令定義。這樣相比通用的lua原代碼,lua字節(jié)碼,clr程序集都更安全些。
- 支持Assembly-CSharp.dll之外的dll的修復(fù)。
- 免代碼生成,更干凈。
它也有缺點(diǎn),不支持新增類,也不支持在已有類新增字段,修bug還是夠用的,但難以通過(guò)熱更為游戲增加新功能。InjectFix就一個(gè)純粹的修bug工具而已。
黑科技
由于InjectFix支持重復(fù)加載補(bǔ)丁,新加載補(bǔ)丁會(huì)自動(dòng)覆蓋上一個(gè),這特性可以用來(lái)實(shí)現(xiàn)真機(jī)代碼邏輯實(shí)時(shí)修改。
(視頻地址:https://v.qq.com/x/page/v09240mo6ai.html?&ptag=4_7.2.5.22206_copy)
蘋果政策合規(guī)性
各熱更方案群的問(wèn)的頻率最高的問(wèn)題之一:這方案會(huì)不會(huì)導(dǎo)致我游戲蘋果審核不通過(guò)。
讓我們看看蘋果的熱更新條款:
可以看到最新條款允許下載代碼解析執(zhí)行,但前提是不能通過(guò)新增特性和功能來(lái)把程序改得(和審核時(shí)相比)面目全非。再看看通常被拒時(shí)的理由中的Guideline2.5.2里的一句:
Yourapp,extension,orlinkedframeworkappearstocontaincodedesignedexplicitlywiththecapabilitytochangeyourapp’sbehaviororfunctionalityafterAppReviewapproval。
有“新增特性和功能”能力的熱更新方案的尷尬之處在于有“改得面目全非”的能力。而InjectFix從它提供的能力(只能修改已有函數(shù))來(lái)看,并不具備“新增特性和功能”的能力,這本來(lái)是弱點(diǎn),放在這里卻成為合規(guī)性的保證了。
基本原理
InjectFix項(xiàng)目的研發(fā)挺曲折的。InjectFix和xLua是同一個(gè)作者,也是本文筆者,當(dāng)時(shí)xLua開(kāi)源后,不斷有人提希望提供個(gè)C#轉(zhuǎn)lua的工具,而深入研究覺(jué)得實(shí)現(xiàn)個(gè)il虛擬機(jī)工作量還更小,這樣還能避免lua的一些gc問(wèn)題。
決定要做il虛擬機(jī)后,也曾想過(guò)直接使用ILRuntime,評(píng)估后覺(jué)得不太符合我們的使用場(chǎng)景:ILRuntime并不能實(shí)現(xiàn)和原生代碼的函數(shù)級(jí)別配合,這是我們能實(shí)現(xiàn)原工程直接改Bug的關(guān)鍵;ILRuntime運(yùn)行時(shí)部分依賴cecil,除了資源占用大之外,還容易和unity自帶或者某些插件的cecil沖突;加載的是標(biāo)準(zhǔn)的程序集在安全性方面也比較堪憂。雖說(shuō)這些都可以改,但修改的工作量也挺大的,還不如自己寫一個(gè)。
InjectFix實(shí)現(xiàn)bug修復(fù)主要靠這兩部分:虛擬機(jī)負(fù)責(zé)新邏輯的解析執(zhí)行;注入代碼負(fù)責(zé)把調(diào)用重定向到虛擬機(jī);下面我們結(jié)合最簡(jiǎn)單的例子介紹下這兩部分。
虛擬機(jī)
關(guān)鍵部分用幾行偽碼就可以描述清楚:
導(dǎo)讀
argumentBase指向的是求值棧該函數(shù)的棧幀,棧幀是這么安排的:
先放參數(shù)(如果有的話),再放本地變量(如果有的話),接著是臨時(shí)區(qū)域,當(dāng)函數(shù)返回時(shí)彈掉所有東西,如果有返回值就放到棧頂(函數(shù)執(zhí)行前參數(shù)0的位置)。
用如下一個(gè)靜態(tài)方法來(lái)演示下虛擬機(jī)怎么運(yùn)行:
這函數(shù)編譯后是這四條指令
Add函數(shù)的執(zhí)行過(guò)程
代碼注入
上面的Add函數(shù)注入后是這樣的
比較簡(jiǎn)單,發(fā)現(xiàn)這函數(shù)有patch的話,就重定向到虛擬機(jī)。
而__Gen_Wrap_25是個(gè)適配器函數(shù),賦值把參數(shù)壓棧,調(diào)用虛擬機(jī)的Execute函數(shù),并把結(jié)果返回。__Gen_Wrap_25的實(shí)現(xiàn)如下:
PS:我們的例子僅有三種指令,和這幾條指令無(wú)關(guān)的代碼全部簡(jiǎn)化了,真正復(fù)雜得多,有興趣可以看源碼了解。
總結(jié)
InjectFix使用簡(jiǎn)單,小巧,合規(guī)且安全。即使你不打算用它來(lái)更新線上版本,只要你程序有原生部分,接入也能一定程度上提高開(kāi)發(fā)效率,沒(méi)什么拒絕它的理由,是吧?
【責(zé)任編輯:張燕妮 TEL:(010)68476606】