Unity的腳本如何跨平臺
想要了解Unity的熱更原理,必須要先了解Unity腳本的編譯和跨平臺機制,通常游戲的跨平臺主要指安卓和IOS端,Unity的官方腳本語言是C#,但也有不少專案會采用C# + Lua語言的方式進行開發,它們主要有三種跨平臺的形式:JIT、AOT、腳本語言,
Unity的C#代碼在代碼被打包時會被編譯器變為成為中間語言IL(Intermediate Language),而不是機器碼(NativeCode,機器的可執行代碼),后續對這些IL的編譯方式不同可以分為AOT和JIT,

JIT(Just In Time)
JIT是一種動態編譯技術,是指Unity打包時將C#編譯成IL后,在運行時.NET JIT編譯器將IL翻譯NativeCode的程序,通常IL由Mono VM編譯執行,這是因為Mono VM中包含了.NET JIT編譯器,
下面是MonoVM的運行流程圖,它就在運行時將IL編譯成機器碼并保存到記憶體中并執行,

AOT(Ahead Of Time)
指在程式程式運行前將代碼變成稱機器碼,它不需要再運行時對代碼進行解釋和編譯,這樣可以提高程式的執行速度和安全性,Unity AOT的跨平臺原理是將程式的C#源代碼在打包時編譯成與平臺無關的IL,然后通過特定的編譯器將IL代碼編譯成特定平臺的Native Code,不同平臺只需要提供對應的編譯器即可,無需在運行時對代碼進行解釋和編譯,從而實作跨平臺,
下面是Unity推薦的IL2CPP編譯器原理圖,在打包時把C#代碼先編譯成IL,再由IL2CPP編譯器編譯成C++代碼,再由特定平臺的C++編譯器編譯成NativeCode,
最后需要L2CPP VM的原因是雖然代碼轉換成了C++代碼,但C#中的記憶體是由GC自動管理,而C++需要手動管理記憶體,因此還需要一個IL2CPP VM用于GC管理等操作,

腳本語言
Lua是一種跨平臺的腳本語言,它主要依賴解釋器和虛擬機實作跨平臺功能,正常的lua腳本跨平臺流程是:
- 撰寫lua腳本
- 使用lua解釋器將lua腳本解釋稱位元組碼
- 由lua虛擬機執行位元組碼
由于解釋器和虛擬機都是跨平臺的,lua腳本也就可以在不同的平臺上運行了,
位元組碼(bytecode)指的是一種中間碼,它是一種介于源代碼和機器碼之間的一種代碼形式,位元組碼是針對特定虛擬機(如Java虛擬機、.NET CLR虛擬機)的指令集,每條指令都比較簡單,并且都能夠被輕易地轉換成機器碼,位元組碼通常是在解釋執行或者即時編譯的程序中生成的,可以有效地提高程式的執行效率和跨平臺能力,
在編譯型語言中,源代碼會被直接編譯成機器碼,而在解釋型語言中,源代碼則會被解釋器逐行執行,相比之下,位元組碼的執行效率通常比解釋器高,但比直接執行機器碼要低,但是,位元組碼的好處在于它可以在多個平臺上運行,只需要在特定平臺上實作一個對應的解釋器或者即時編譯器即可,這也是為什么很多跨平臺的語言(如Java和Lua)都采用了位元組碼的形式,
舉個例子,Java源代碼在編譯時會被轉換成Java位元組碼,然后在JVM上解釋執行或者即時編譯成機器碼,這樣,Java程式就可以在不同的作業系統和硬體上運行,只需要在不同平臺上實作對應的JVM即可,
另外,lua也提供了JIT版本,以便在運行時將lua代碼編譯成NativeCode執行,與普通的lua解釋器相比,可以顯著地提升lua代碼的執行速度,
C#如何熱更新
Lua腳本語言是解釋執行的,運行前加載更新后的代碼就可以達到熱更新的效果,在本文中要說明的是C#的熱更新,
理想化的熱更新流程是:
- 把需要更新的代碼編譯成元件
- 游戲啟動時加載新的元件
- 用反射的形式獲取元件中的實體或方法
這種模式在PC和Android平臺是可以的,但在IOS平臺是不可行的,因為IOS對申請的記憶體禁止了可執行權限,所以運行時創建/加載的NativeCode是無法執行的,
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize)函式是申請記憶體的函式,它的第三個引數是代表了申請記憶體的保護方式:
- PROT_EXEC 映射區域可被執行
- PROT_READ 映射區域可被讀取
- PROT_WRITE 映射區域可被寫入
而IOS平臺是不支持PROT_EXEC的,運行時申請的記憶體不可執行,所以這種理想化的熱更新流程無法在IOS平臺起作用,
詳細原因可以看下這篇文章,
為了解決IOS上的熱更新問題,有兩個主流方案:ILRuntime 和 HybridCLR,
ILRuntime
Unity會把C#代碼打包成DLL,ILRuntime在運行時用自己的解釋器來解釋IL并執行,而不是直接呼叫.NET FrameWork或Mono虛擬機來運行代碼,它借助Mono.Cecil庫來讀取DLL的PE資訊,以及當中型別的所有資訊,最終得到方法的IL匯編碼,然后通過內置的IL解譯執行虛擬機來執行DLL中的代碼,
但是ILRuntime會有一些限制,見https://ourpalm.github.io/ILRuntime/public/v1/guide/FastQA.html,
- ILRuntime和原始的 compiler是兩套東西,也就是說你的熱更DLL和主工程的DLL實質是不互通的(如熱更DLL中一個類要繼承主工程DLL的一個類),所以就存在跨域問題,需要寫委托配接器,委托轉換器,在發布版本后這些不能熱更,使用之前一定要預留好可能會使用的
- 部分 C# 語法不支持:由于 ILRuntime 是基于 Mono 實作的,而 Mono 不支持所有 C# 語法,所以 ILRuntime 在某些 C# 語法方面也有限制,比如屬性、泛型委托、可選引數等
- 需要特殊處理的代碼:由于 ILRuntime 的實作方式,一些特殊的代碼需要進行特殊處理,比如反射、LINQ、協程等
- 性能問題:由于 ILRuntime 需要動態決議和執行代碼,相對于編譯時靜態系結的方式,其性能會有一定程度的下降,同時,在使用程序中也需要注意避免頻繁的跨域呼叫和反射操作,以免影響性能
- ILRuntime對多執行緒Thread不兼容,在熱更代碼里使用多執行緒會導致Unity崩潰閃退
HybridCLR
是一個特性完整、零成本、高性能、低記憶體的近乎完美的Unity全平臺原生c#熱更方案,
IL2CPP是一個純靜態的AOT運行時,不支持運行時加載dll,因此不支持熱更新,HybridCLR擴充了IL2CPP的代碼,使其由純AOT Runtime變成“AOT+Interpreter”混合Runtime,進而原生支持動態加載Assembly,使得基于IL2CPP打包的游戲不僅能在Android平臺,也能在IOS、Consoles等限制了JIT的平臺上高效地以AOT+interpreter混合模式執行,
ILRuntime是引入一個第三方VM(Virtual Machine),在VM中解釋執行代碼,來實作熱更新,這些熱更新方案的VM與IL2CPP是獨立的,意味著它們的元資料是不相通的,在熱更新里新增一個型別是無法被IL2CPP所識別的例如通過System.Activator.CreateInstance是不可能創建出這個熱更新型別的實體),這種看起來像、實際上卻又不是的偽CLR虛擬機,在與IL2CPP這種復雜的CLR運行時互動時,產生極大量的兼容性問題,另外還有嚴重的性能問題,
HybridCLR對IL2CPP運行時進行擴充,添加interpreter模塊,進而實作像Mono一樣的混合執行模式,這樣一來就能徹底支持熱更新了,并且兼容性極佳,對開發者來說,除了以解釋模式運行的部分執行得比較慢,其他方面跟標準的運行時沒有區別,
通俗地說,il2cpp相當于Mono的AOT模塊,HybridCLR相當于Mono的interpreter模塊,兩者合一成為完整Mono,HybridCLR使得IL2CPP變成一個全功能的Runtime,原生(即通過System.Reflection.Assembly.Load)支持動態加載dll,從而支持ios平臺的熱更新,

正因為HybridCLR是原生Runtime級別實作,熱更新部分的型別與主工程AOT部分型別是完全等價并且無縫統一的,可以隨意呼叫、繼承、反射、多執行緒,不需要生成代碼或者寫配接器,而其他熱更新方案則是獨立VM,與IL2CPP的關系本質上相當于Mono中嵌入lua的關系,因此型別系統不統一,為了讓熱更新型別能夠繼承AOT部分型別,需要寫配接器,并且解釋器中的型別不能為主工程的型別系統所識別,特性不完整、開發麻煩、運行效率低下,
HybirdCLR的原理細節Walon有分享,可以在其知乎專欄查看,
參考
- ILRuntime中文官網,https://ourpalm.github.io/ILRuntime/public/v1/guide/principle.html
- 曾志偉, 【Unity游戲開發】Mono和IL2CPP的區別, https://zhuanlan.zhihu.com/p/352463394
- HybridCLR介紹,walon,https://www.zhihu.com/question/519548488
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/551739.html
標籤:其他
上一篇:「學習筆記」AC 自動機
下一篇:返回列表
