《介紹一種基于Mono的Unity熱更新方案》
熱更新是Unity3D開發(fā)總也繞不過去的話題矢渊,甚至影響到了開發(fā)語言战转,程序架構(gòu)、人員配置予弧,不可謂不重要刮吧。文章開頭先從一些大家都熟知的東西帶入。熱更新目前有很多成熟的方案掖蛤,筆者很早前因為工作需要了解了一些信息杀捻,大體分幾個流派
- Lua流派/CSharp轉(zhuǎn)Lua流派
- CSharp流派
- JS/TS流派
各個流派均有成熟的框架,優(yōu)劣勢在此不再展開蚓庭,選擇時往往是結(jié)合自己團隊的情況來取舍致讥。從方向上看仅仆,筆者更看好Lua流派,Lua天生就作為腳本語言設(shè)計垢袱,集成到游戲引擎中作為邏輯腳本似乎是一件很合理的事情蝇恶。筆者對Lua不是很熟悉,也曾因此在工作面試中被鄙視惶桐,從個人喜好上撮弧,還是喜歡CSharp這門語言多一點,當然這個喜好也是建立在特定環(huán)境下的姚糊,語言層面的優(yōu)劣在此也不再展開贿衍。在聊新的方案前,先從頭聊一些熱更新方面的知識做引子救恨。
熱更新的重災區(qū)是在iOS系統(tǒng)贸辈,因為一些眾所周知的原因,Unity最初的Mono運行時在iOS平臺下只能以full AOT模式運行肠槽,這樣就無法實現(xiàn)熱更新了梁呈。這里也引出了運行模式的概念,大家熟知的有:
-
JIT運行
Mono髓抑,V8等引擎默認運行模式胜榔,這種模式是可以動態(tài)Load代碼的,也就是可以更新代碼邏輯寂纪。但是在iOS系統(tǒng)上是被禁止的席吴。
-
AOT運行
提前編譯成本機代碼,運行效率可以比肩原生代碼捞蛋,Unity的Mono引擎在iOS系統(tǒng)上即以此模式運行孝冒,但是不能更新代碼。
-
Interpreter運行
即解釋器執(zhí)行拟杉,顧名思義庄涡,腳本語言以此方式運行,并沒有生成本地機器碼搬设,目前各個熱更流派均是以此方式實現(xiàn)熱更新穴店,代價是效率低一些。
Lua天生是以解釋器運行的焕梅,具有體積小迹鹅,集成靈活等特性作為大家的首選腳本,也發(fā)展出了jit模式來解決其他平臺上的性能問題贞言,有xLua斜棚,toLua等成熟框架。
CSharp也發(fā)展出了ILRuntime框架來支持解釋模式,從而實現(xiàn)了CSharp熱更新弟蚀。
那么JS/TS呢蚤霞,筆者以前以為V8引擎一直有解釋器的,不然iOS上的Chrome是怎么運行的呢义钉?帶著這個問題查了下才發(fā)現(xiàn)V8確實加了解釋器昧绣,并不是很久以前。所以現(xiàn)在JS/TS流派也發(fā)展出了成熟的框架比如Puerts捶闸。
那么Mono呢夜畴?再繼續(xù)查了下,也有删壮。Mono的解釋器命運就比較曲折了贪绘,從Mono第一個版本便有,再到后來光榮退場央碟,然后重新出山税灌,當然是肩負了使命的。既然再加回來當然還要再進一步亿虽,AOT和Interpreter都可以在iOS上運行菱涤,如果可以讓熱點代碼跑在AOT,容易變更的代碼跑在Interpreter洛勉,兩部分代碼不需要關(guān)心自己的運行時不是更好嗎粘秆?再繼續(xù)查了下,也有坯认,Mono內(nèi)部已經(jīng)實現(xiàn)了兩套運行模式的交互部分翻擒,在aot編譯時提前生成了交互代碼氓涣,運行時的代碼可以無感知的相互調(diào)用牛哺,并且完善度已經(jīng)相當高。
-
Mixed-Mode Execution
Mono支持的一種運行模式劳吠,混合了AOT和Interpreter引润,在執(zhí)行沒有AOT的程序集時,自動將程序集切換到Interpreter內(nèi)執(zhí)行痒玩,所以支持動態(tài)Load代碼淳附。
事情在朝著好的方向發(fā)展,似乎一切都比較合理蠢古。從mono的提交日志看奴曙,2018年開始充斥著大量的[interp]模塊提交,幾乎占到了總提交量的1/3草讶,mono的這個模塊發(fā)展很快洽糟。反觀Unity官方mono,恰好停留在[interp]模塊加入前,便不再合并mono主干坤溃。具體原因我們不再此猜測拍霜,只是這樣就需要我們自己動手了。
既然運行時已經(jīng)支持薪介,剩下的工作就是集成到UnityEngine內(nèi)與Il2cpp親密無間祠饺。在此之前我們先以Unity的默認執(zhí)行框架做引子,以下為筆者個人理解汁政,不正確的地方請以官方為準道偷。
1號通道最初是通過Mono的Internal call來實現(xiàn)的,Il2cpp同樣使用此方式來實現(xiàn)(C)到(A)的請求记劈。
2號通道是UnityEngine通過Il2cpp 然后invoke上層接口來實現(xiàn)回調(diào)试疙。
0號通道我們先稱之為magic,實現(xiàn)一些定制特性抠蚣,我們先忽略祝旷。
如果要在Unity項目中實現(xiàn)Mono 的Mixed-Mode Execution,我們需要在此系統(tǒng)內(nèi)再加入一個Mono runtime嘶窄,同時綁定上述三條通道怀跛,這里先說下我們的第一種綁定方案(icall綁定):
- 針對1號通道,Mono原生即支持Internal call(我們簡稱icall)柄冲,那么在Mono中直接執(zhí)行unity assembly吻谋,然后將icall調(diào)用直接指向(A)即是最直接的方式。
- 針對2號通道需要在(B)層做些手腳现横,通過查代碼我們發(fā)現(xiàn)Il2cpp的Method實際是一個函數(shù)指針漓拾,那么查找到需要回調(diào)的函數(shù),并使指針指向我們的實現(xiàn)戒祠,然后再Invoke Mono內(nèi)的相同函數(shù)即實現(xiàn)了hook功能骇两,即實現(xiàn)了UnityEngine的回調(diào)。
通過以上兩種方式我們在自己的Mono runtime內(nèi)綁定了大部分的Unity功能姜盈。為什么是大部分呢低千,這里可以實現(xiàn)icall綁定的前提是所有icall綁定傳遞的對象只有一份內(nèi)存,并且是在(A)內(nèi)馏颂,UnityEngine.Object即是此目的示血。Unity當然不會就此收手,magic就無用武之地了救拉,除此還有其他一些特性难审,最麻煩的是0號magic通道,比如MonoBehaviour亿絮、Coroutine告喊、傳遞給(A)一個.Net 的Stream等等拂铡。這里因為Unity做了一些特殊處理,具體實現(xiàn)我們不得而知葱绒,即使勉強實現(xiàn)了也無法保證以后兼容性感帅,所以我們使用了Wrapper的方式。這里有兩種實現(xiàn)方式地淀,后面再詳細介紹失球。
我們再來看看加上Mono runtime后的結(jié)構(gòu):
如上面所說,我們需要綁定(H)內(nèi)的icall指向(A)即新增了通道5-3帮毁,同時需要hook(B)內(nèi)的函數(shù)指針實現(xiàn)回調(diào)实苞,即新增了通道4-5。
同時為了處理magic情況烈疚,我們提供兩種方案黔牵,一種是手動在(F)內(nèi)實現(xiàn)綁定接口(Unity的icall綁定大部分也是這種無規(guī)則的手動實現(xiàn),所以給我們的自動綁定帶來了很多麻煩)這種方案上層用戶(I/K)完全無感知爷肝,只是因為這部分是由c/cpp實現(xiàn)猾浦,對部分團隊并不友好。所以我們新增了通道6灯抛,也就是我們的第二種綁定方案(Adapter綁定):
Adapter是指在(D)中指定一些需要在(I/K)內(nèi)使用的程序集金赦,在構(gòu)建時為這些程序生成兩個Adapter程序集,分別位于(E)和(J)对嚼,這樣當用戶(I/K)調(diào)用(D)內(nèi)的程序接口時會自動通過通道6調(diào)用夹抗,調(diào)用方無感知,同時通道6是雙向的纵竖,即同時支持調(diào)用與回調(diào)漠烧。
另外指出的是(H)是直接使用的UnityEngine.*.dll,只需重新綁定icall即可靡砌,(F)/(E)/(J)內(nèi)的綁定代碼均由代碼生成器生成已脓,即除非需要手動實現(xiàn)icall綁定,通道3/4/5/6均自動生成乏奥。
兩種方案是否給框架增加了復雜性呢摆舟,其實在開發(fā)過程中,為了保持簡潔筆者在這兩種方案中反復切換了多次邓了,每個單獨方案都能實現(xiàn)絕大部分的功能,但是總會讓一小部分特定的問題復雜化媳瞪。比如我們?nèi)渴褂肁dapter綁定可以完成需求嗎骗炉?其實是可以的,但是碰到Unity使用runtime來支持的特性蛇受,單純的從CSharp層來實現(xiàn)復雜度會大大增加句葵,或者需要用戶修改程序,而且后續(xù)功能的兼容和擴展性會低很多。兩種方式一起用乍丈,雖然給綁定生成器帶來了復雜性剂碴,用戶使用反而簡單一些,所以保留了兩種綁定方案轻专。
至于用戶的程序集是在(K)內(nèi)執(zhí)行還是在(I)內(nèi)執(zhí)行忆矛,用戶可以自己根據(jù)實際需求來配置,綁定生成器會在構(gòu)建時自動觸發(fā)请垛,根據(jù)配置生成不同的工程催训,然后將此工程以pod庫的形式提供給主項目集成。主項目需要在podfile中引用后執(zhí)行
pod install
即可鏈接成最終執(zhí)行項目宗收,以上即是筆者本次介紹的方案漫拭,詳細使用細節(jié)請移步這里。此方案支持iOS平臺下Assembly.Load接口混稽。Android平臺建議直接使用Unity的Mono運行時采驻,同樣支持Assembly.Load接口,這樣在架構(gòu)上不需改動匈勋。
此方案其實構(gòu)思已久挑宠,期間做了不少可行性測試,一直因抽不出時間拖著未實現(xiàn)颓影,最終也因2020年這個年終閑的時間長了些才得空實現(xiàn)了出來各淀,其間縫合多個程序邊界并實現(xiàn)自動化的復雜度還是超出了預期,總算最終走通了诡挂,因為感覺到自己可以調(diào)配的精力非常有限碎浇,也深知獨立開發(fā)很難使這個框架完善,所以決定開源出來璃俗,也順便取了個名字:PureScript奴璃,起碼保持從用戶角度看來是一個簡潔、單純的腳本框架城豁。
如果大家有興趣苟穆,后面再補充詳細實現(xiàn)細節(jié),目前項目已經(jīng)開源唱星。對此方案有興趣的同學歡迎提交PR或者Star雳旅。
添加一個錄屏