前言:主要記錄了在接入華佗熱更新中的一些思考和對整個項目的設(shè)計墓律,當(dāng)然也包括接入過程中的一些坑膀估。希望可以給需要接入熱更新的人一些啟發(fā),同時也歡迎大家留下自己的的建議和想法耻讽。
一. 什么是HybridCLR察纯?
HybridCLR(原華佗熱更新) 是一個 特性完整、零成本针肥、高性能饼记、低內(nèi)存 的近乎完美的Unity全平臺原生c#熱更方案。
沒有人比自己更了解自己的代碼慰枕,所以想了解更詳細(xì)的內(nèi)容具则,可見官方鏈接:
https://focus-creative-games.github.io/hybridclr/about/
二. 為什么使用HybridCLR?
- 使用HybridCLR熱更新捺僻,可以以最小的成本接入項目乡洼,無需對項目進(jìn)行大規(guī)模的重寫和修改崇裁。
- HybridCLR熱更新針對il2cpp進(jìn)行了拓展,可以直接使用C#語言開發(fā)束昵,無需使用lua拔稳,typescrpt等腳本語言。
- AOT代碼和JIT代碼使用C#同一套語言锹雏,并且對于跨域繼承巴比,反射等高級語言特性支持完善,也無需使用多個項目進(jìn)行熱更代碼的邏輯分離礁遵,對于開發(fā)的便捷性有著很大的提升轻绞。
- 在執(zhí)行效率和內(nèi)存占用等性能指標(biāo)上也有著大幅的的優(yōu)勢。
所以為什么使用HybridCLR佣耐?這里可以用它的創(chuàng)造者walon的自負(fù)且自信的一句話回答:
HybridCLR是一個劃時代的Unity平臺C#原生熱更新技術(shù)政勃,它將國內(nèi)Unity開發(fā)的技術(shù)框架水平提高到新的高度,并深刻地改變Unity平臺的開發(fā)生態(tài)兼砖。
三. 如何接入HybridCLR奸远?
關(guān)于如何接入HybridCLR,官方文檔已經(jīng)很詳盡了讽挟,所以具體的接入操作可以去上面官方文檔中查看懒叛。這里只介紹下接入過程中的一些問題,具體的操作不再贅述耽梅。
推薦使用unity2020.3.33版本薛窥,其他版本可能有些問題或者額外操作,等熟悉后再嘗試接入其他版本的unity眼姐。
接入流程主要分為以下步驟:
- 安裝 hybridclr_unity package诅迷,見官方文檔: https://focus-creative-games.github.io/hybridclr/install/ ;
- 配置PlayerSettings中的設(shè)置妥凳,見官方文檔 https://focus-creative-games.github.io/hybridclr/project_settings/ 竟贯;
- 根據(jù)需求劃分程序集,關(guān)于如何劃分程序集的更多的內(nèi)容參考下面的章節(jié): 六.程序集設(shè)計逝钥;
- 配置HybridCLR的參數(shù)屑那,新手主要關(guān)注 hotUpdate Assembly Definitions (需要熱更的程序集,根據(jù)步驟3中的劃分填寫) 和 hotUpdate dlls(需要熱更的dll) 兩個參數(shù)艘款;
- 到這里持际,不涉及到寫代碼部分的操作已經(jīng)完成,接下來的內(nèi)容就于代碼相關(guān)了哗咆。
四. 熱更新的流程設(shè)計
在寫熱更新代碼前蜘欲,先來梳理下熱更新的流程吧。
步驟 0~2
如上圖中所示晌柬,第0~2步是常規(guī)的下載-加載資源流程姥份,這些部分根據(jù)項目隨意發(fā)揮郭脂,當(dāng)然我們的熱更dll也是在這里需要打包成ab包下載下來的。
步驟 3 加載熱更dll
加載熱更的dll很簡單澈歉,只需要加載資源展鸡,然后調(diào)用Assembly.Load() 即可。
// 加載資源 (示范代碼)
var dllBytes = await AssetComponent.LoadAsync<TextAsset>("Assets/Dlls/game.bytes");
// 加載程序集
Assembly gameAss = System.Reflection.Assembly.Load(dllBytes.bytes);
步驟 4 加載補(bǔ)充元數(shù)據(jù)dll
加載補(bǔ)充元數(shù)據(jù)dll埃难。這一步是可選的莹弊,但是如果熱更程序集引用到了aot程序集的情況下,這一步是必不可少的涡尘,不然很大幾率我們會看到:
MissingMethodException: AOT generic method isn't instantiated in aot module xxx
上面這個錯誤就是因為AOT泛型函數(shù)實例化缺失導(dǎo)致的忍弛,所以我們最好補(bǔ)充元數(shù)據(jù)。補(bǔ)充元數(shù)據(jù)的代碼如下:
// 前三個是系統(tǒng)自帶考抄,推薦帶上细疚,第四個是自己游戲AOT的程序集
var AotAssemblyDlls = new string[] {
"mscorlib","System","System.Core",
"MyGame.Framwork",
};
for (int i = 0; i < AotAssemblyDlls.Length; i++)
{
var aotDllName = AotAssemblyDlls[i];
var path = $"Assets/ResBundles_Dll/aotDll/{aotDllName}.bytes";
var asset = AssetComponent.Load<TextAsset>($"Assets/AotDlls/{aotDllName}.bytes");
// 加載assembly對應(yīng)的dll,會自動為它hook座泳。一旦aot泛型函數(shù)的native函數(shù)不存在惠昔,用解釋器版本代碼
LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(asset.bytes);
if (err == LoadImageErrorCode.OK)
Debug.Log($"{stateID}: LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}");
else
Debug.LogError($"{stateID}: LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}");
}
到這里可能很多人疑問幕与,我們的元數(shù)據(jù)dll從哪來的挑势?
其實在打包build過程中,會生成裁剪過的dll啦鸣,HybirdCLR會自動幫我們把dll拷貝到目錄:項目目錄/HybridCLRData/AssembliesPostIl2CppStrip/目標(biāo)平臺/xxx 潮饱。
我們也可以通過以下代碼獲取當(dāng)前目標(biāo)平臺的目錄:
SettingsUtil.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
所以如果通過ab加載,在打包assetbundle資源前诫给,我們必須要進(jìn)行一次構(gòu)建操作香拉。然后把構(gòu)建生成的需要補(bǔ)充的dll拷貝到項目目錄中,然后根據(jù)自己的ab打包策略中狂,將dll打包成ab資源凫碌。
步驟 5 加載熱更場景,執(zhí)行熱更代碼
因為AOT代碼無法直接調(diào)用熱更代碼胃榕,那么如何運(yùn)行熱更dll里的啟動代碼呢盛险?推薦以下為比較好的方案:
推薦方案:在所有資源都已下載,所有熱更代碼都已加載后勋又,通過加載熱更資源中的場景苦掘,場景上掛有熱更腳本(例如InitAfterHotFix.cs),在InitAfterHotFix的Awake方法中進(jìn)行后續(xù)游戲的初始化楔壤。
其他方案:當(dāng)然也可以通過Assembly類獲取到熱更程序集的啟動類的啟動方法鹤啡,使用反射的方式實現(xiàn)。
五. 打包流程
在以上步驟已處理完畢蹲嚣,代碼添加好了以后递瑰,下面將開始打包的流程祟牲。
打包流程主要分成以下幾步:
編譯熱更新Dll
unity中HybridCLR->CompileDll->ActiveBuildTarget按鈕,或者直接調(diào)用代碼: CompileDllCommand.CompileDll(EditorUserBuildSettings.activeBuildTarget);把編譯出來的熱更Dll拷貝到項目里抖部,按照自己的ab打包策略放入自己規(guī)劃的目錄內(nèi)疲眷;
-
生成LinkXml,MothedBridge等等
unity中HybridCLR->Generate->All按鈕您朽,或者直接調(diào)用代碼: HybridCLR.Editor.Commands.PrebuildCommand.GenerateAll();
-
拷貝AOT補(bǔ)充元數(shù)據(jù)dll到項目里狂丝,按照自己的ab打包策略放入自己規(guī)劃的目錄內(nèi);
補(bǔ)充元數(shù)據(jù)dll的需要經(jīng)過一次打包后生成哗总,具體生成邏輯和文件目錄在第四節(jié)中有介紹几颜。
打包AssetBundle資源;
根據(jù)自己的需求讯屈,將ab資源放入StreamingAssets或遠(yuǎn)程服務(wù)器蛋哭。
六. 程序集設(shè)計
程序集設(shè)計雖然放在最后一章節(jié),但是應(yīng)該是項目最開始就優(yōu)先考慮和設(shè)計的涮母。
6.1 劃分程序集谆趾,不僅僅是為了熱更新
程序集的劃分,不僅僅為了熱更新叛本。在不需要熱更新的項目中沪蓬,合理的規(guī)劃和拆分代碼模塊,設(shè)置合理的引用關(guān)系来候,可以解除基礎(chǔ)框架-游戲模塊-三方插件的耦合跷叉。
如上圖中游戲模塊作為需要經(jīng)常改動的模塊,它引用了基礎(chǔ)框架模塊和三方插件营搅。而后者沒有對其的引用云挟。
如果按上圖劃分程序集并設(shè)置引用關(guān)系:代碼的依賴關(guān)系會因為程序集依賴被強(qiáng)制限制。如果某個不規(guī)范的程序員擅自在基礎(chǔ)框架中添加了涉及到游戲模塊的代碼转质,那么Ta會發(fā)現(xiàn)代碼報錯园欣,編輯器無法在基礎(chǔ)框架程序集中查到對游戲模塊程序集的代碼的引用。
如果基礎(chǔ)框架和游戲模塊同在同一個程序集中:代碼的依賴關(guān)系必須要靠個人的代碼能力管理休蟹。作為管理者必須要嚴(yán)格規(guī)范下屬并經(jīng)常review代碼沸枯,不然不規(guī)范的下屬幾天就可能把基礎(chǔ)框架和游戲模塊揉合成一團(tuán)小貓玩過的毛線團(tuán),很難理順鸡挠!
6.2 程序集與熱更新
在熱更新需求下的程序集劃分與普通的項目差別很小辉饱,基本通用,主要在于我們?nèi)绾蝿澐諥OT程序集和JIT程序集拣展,而且還有對于unity默認(rèn)的Assembly-CSharp程序集的定義彭沼。
例如上面的6.1中圖片中的例子,我們可以有2種劃分方案:
AOT程序集包括基礎(chǔ)框架模塊和三方插件备埃,熱更程序集包括游戲模塊的1~N個程序集姓惑。這種方案下適用于基礎(chǔ)框架穩(wěn)固完善的項目褐奴,因為基礎(chǔ)框架不會有大的變動所以不需要頻繁更新整包;
AOT程序集僅包括三方插件(甚至三方插件也可以不包括)于毙,剩余所有的程序集作為熱更敦冬。這種方案下基本所有內(nèi)容都可以熱更,適用于新項目的快速迭代唯沮。
但是劃分方案2中脖旱,有個問題:
- 下載/加載ab資源等操作,是在需要熱更的基礎(chǔ)框架中介蛉;
- 加載熱更資源和加載dll等初始化操作萌庆,是在AOT程序集中,且會用到下載/加載ab資源代碼币旧;
所以在上面的幾條前提下践险,我們發(fā)現(xiàn)這種劃分方案下:AOT程序集中有很多對熱更程序集中代碼的調(diào)用。
我們可以通過Assembly加載吹菱,通過反射調(diào)用到熱更的代碼巍虫,但是這樣必然很繁瑣。還有另外一種方法鳍刷,為了避免AOT引用熱更代碼占遥,我們也可以在AOT中實現(xiàn)另外一套資源下載/加載邏輯。當(dāng)然這2種方法都很麻煩倾剿,最好還是基礎(chǔ)框架的代碼(至少下載/加載ab資源的代碼)設(shè)置為AOT程序集筷频。
6.3 Assembly-CSharp程序集
在上述2種方案中,我們都要考慮一個特殊的程序集:unity默認(rèn)的Assembly-CSharp程序集前痘。
如果自己添加的代碼,如果沒有專門劃分程序集担忧,那么就會被設(shè)置為默認(rèn)的Assembly-CSharp或Assembly-CSharp-Editor程序集芹缔。
默認(rèn)程序集的特殊之處
不確定性和混沌性:自定義的程序集必定包含在某個專屬的文件夾下或dll中,但是默認(rèn)程序集的代碼遍布整個項目目錄瓶盛,藏于各個犄角旮旯之中最欠,不便管理和統(tǒng)計。
擁有最大訪問權(quán)力:自定義的程序集需要設(shè)置對外部程序集的引用后惩猫,才可以在內(nèi)部使用相關(guān)的接口代碼芝硬。但是默認(rèn)程序集可以訪問項目內(nèi)所有的程序集(在勾選Auto Referenced的時候),它相當(dāng)于是處于程序集引用關(guān)系的金字塔頂部轧房。
熱更中默認(rèn)程序集是否熱更拌阴?
以下兩種方案都可以,但是各有利弊:
- 我們可以將其定義為AOT程序集奶镶,不引用任何熱更程序集迟赃,那么下載熱更資源陪拘,加載dll等操作均在這里實現(xiàn)。
- 我們也可以將其定義為熱更程序集纤壁,那么下載熱更資源左刽,加載dll等操作均在劃分的其他AOT程序集中實現(xiàn)。
把Assembly-CSharp程序集當(dāng)作熱更程序集有個不好的地方酌媒,那便是我們隨意加入的測試代碼(沒有放在有程序集定義的文件夾下)欠痴,某些導(dǎo)入的三方插件等等,只要未定義過程序集秒咨,那么默認(rèn)就會加入到Assembly-CSharp程序集斋否。所以會經(jīng)常誤把某些代碼打入熱更dll。為避免程序花費(fèi)時間和精力去檢查這些內(nèi)容拭荤,不建議用這種方案茵臭。
6.4 unity里的Assembly Definition文件
在一個普通的C#項目中,我們可以在vs中右鍵程序集(項目)條目然后打開屬性舅世,查看該程序集的相關(guān)設(shè)定旦委。但是unity生成的項目中,我們是無法使用這個操作的雏亚。
因為程序集的生成和劃分是由我們在unity中處理缨硝,然后unity自動生成。其中unity中用來劃分程序集的文件就是Assembly Definition罢低。
我們通過Assembly Definition文件查辩,可以處理以下事情:
- 添加自定義的宏:Define Constraints;
- 添加刪除程序集的依賴:Assembly Definition Refercences;
- 選擇生效的平臺:Platforms;
除了上面這些常用的功能,還有General選項下這些很有用的選項:
- Allow ‘unsafe’ Code:是否啟用不安全的代碼网持;
- Auto Referenced:是否自動被依賴宜岛;勾選后會被默認(rèn)的Assembly-CSharp程序集自動依賴。所以如果我們想在Assembly-CSharp中隔離對當(dāng)前程序集的依賴功舀,取消勾選萍倡。
- No Engine References:不依賴于引擎提供的代碼模塊。適用于可以在unity或其他平臺的項目中通用的程序集辟汰。
- Override References:可以手動指定所依賴的預(yù)編譯的程序集列敲,因為unity項目中的預(yù)編譯程序集可以被其他默認(rèn)依賴,勾選后當(dāng)前程序集可以選擇(不)依賴某個預(yù)編譯的程序集帖汞。
- Root Namespace: 當(dāng)前程序集的默認(rèn)命名空間戴而,填寫后我們使用unity添加新代碼文件,會自動添加命名空間翩蘸。我測試只有在unity中創(chuàng)建才生效所意。