1、IL2CPP組成:
(1)AOT編譯器(il2cpp.exe)
unity中IL2CPP編譯步驟如下:
a、將 Unity Scripting API 代碼編譯為常規(guī) .NET DLL(托管程序集)。
b、應(yīng)用托管字節(jié)碼剝離。此步驟可顯著減小構(gòu)建的游戲大小。
c、將所有托管程序集轉(zhuǎn)換為標(biāo)準(zhǔn) C++ 代碼。
d、使用本機(jī)平臺編譯器編譯生成的 C++ 代碼和 IL2CPP 的運(yùn)行時(shí)部分。
e、將代碼鏈接到可執(zhí)行文件或 DLL,具體取決于目標(biāo)平臺。
(2)運(yùn)行時(shí)庫(libil2cpp)
il2cpp頭文件和源碼位置:
Unity安裝目錄下:Unity\Editor\Data\il2cpp\libil2cpp
2、IL2CPP特點(diǎn):
(1)針對C#中計(jì)算密集型代碼性能相比Mono提高很多
(2)只支持AOT編譯
(3)相對于Mono,構(gòu)建時(shí)間更長,生成代碼量更大
3、IL2CPP生成的C++代碼解析(Unity版本:2019.4.23)
(1)成員變量
由上圖可以看出,il2cpp將成員變量的生成分為兩個(gè)結(jié)構(gòu)體,一個(gè)結(jié)構(gòu)體包含所有普通成員變量,另一個(gè)包StaticFields包含所有的靜態(tài)成員變量。每一個(gè)成員變量都會(huì)生成一個(gè)get和一個(gè)set的內(nèi)聯(lián)函數(shù)。
因?yàn)殪o態(tài)成員是所有實(shí)例共享的數(shù)據(jù),因此在運(yùn)行的時(shí)候,Varient_t75DD1618D504050BE978835922A46B83C2969565_StaticFields只有一份。所有的Varient_t75DD1618D504050BE978835922A46B83C2969565實(shí)例都共享這個(gè)數(shù)據(jù)。調(diào)用時(shí)如上圖所示,在Varient_t75DD1618D504050BE978835922A46B83C2969565的元信息結(jié)構(gòu)中有一個(gè)指向Varient_t75DD1618D504050BE978835922A46B83C2969565_StaticFields結(jié)構(gòu)的指針,獲取staticfield指針再調(diào)用對應(yīng)的get方法。
(2)普通成員函數(shù):
(3)靜態(tài)方法
如上面幾個(gè)圖所示,所有的函數(shù)都被聲明成了IL2CPP_EXTERN_C也就是extern “C”類型,這樣一來,在需要的時(shí)候就可以騙過C++編譯器讓其認(rèn)為所有這些函數(shù)都是一個(gè)類型。
所有的函數(shù)都不是成員函數(shù)。函數(shù)的第一個(gè)參數(shù)永遠(yuǎn)都是“this”指針。對于托管代碼中的靜態(tài)函數(shù),IL2CPP會(huì)忽略這個(gè)參數(shù)也就相當(dāng)于傳遞NULL作為第一個(gè)參數(shù)的值。這么做的好處是可以讓il2cpp.exe轉(zhuǎn)換代碼的邏輯更加簡單并且讓代理函數(shù)的處理變得更加容易。所有的函數(shù)還有一個(gè)額外的RuntimeMethod*參數(shù)用來描述函數(shù)的元信息。這些元信息是虛函數(shù)調(diào)用的關(guān)鍵。Mono使用和特定平臺相關(guān)的方法來傳遞這些元信息。而IL2CPP出于可移植方面的考慮,并沒有使用這些和平臺相關(guān)的特定代碼。在this指針和RuntimeMethod*之間的就是函數(shù)實(shí)際用到的參數(shù)。
托管代碼中的類型會(huì)被加上“_t”的后綴,函數(shù)則是加上“_m”后綴和一個(gè)唯一的MD5碼來避免名字的重復(fù)。
(4)異常處理
托管代碼中的異常都會(huì)被il2cpp轉(zhuǎn)換成C++的異常。如圖3-11,所有托管異常都被封裝在C++中的IL2CppExcetionWrapper類型,當(dāng)C++代碼捕獲異常后,會(huì)解析獲取對應(yīng)的托管異常,通過il2cpp_codegen_class_is_assignable_from()判斷該異常是否要捕獲的異常,如果是則跳轉(zhuǎn)至Catch代碼塊執(zhí)行,否則拋出一個(gè)C++異常。
(5)goto
如圖3-11所示,我們發(fā)現(xiàn)生成的C++代碼中包含goto語句。這是因?yàn)镮L中沒有while、do和for循環(huán)以及if/else結(jié)構(gòu),它們都是使用標(biāo)簽、相等goto與條件goto語句實(shí)現(xiàn)的。所以il2cpp在處理IL代碼時(shí),也直接使用標(biāo)簽+goto的方式實(shí)現(xiàn)。
(6)il2cpp.exe不針對每一個(gè)類中的函數(shù)生成單獨(dú)的一個(gè)cpp文件,而是將很多類的函數(shù)放在一個(gè).cpp文件。如上圖,Assetbly-CSharp中所有的函數(shù)被生成在5個(gè).cpp文件中。il2cpp之所以這么做的原因是C++編譯器在編譯相同代碼量的情況下,處理大量的文件的用時(shí)比集中處理少量文件長很多。
(7)泛型共享
il2cpp針對泛型參數(shù)是引用類型還是值類型生成不同的共享代碼。
對于引用類型的泛型共享,由于所有托管代碼中的引用類型都繼承自System.Object,轉(zhuǎn)成C++代碼后都能用RuntimeObject *指針表示。所以所有引用類型都可以共享一份代碼實(shí)現(xiàn)。
而對于值類型,泛型T的大小不同,只能針對不同的值類型生成特有的代碼。
如圖3-14,當(dāng)泛型類型是string和AnyClass兩個(gè)引用類型,最終調(diào)用的構(gòu)造函數(shù)是同一個(gè)函數(shù)即GenericType_1__ctor_mAF3DF5964EBFD3F6A60BB493C66276FA1FCF431B_gshared。
而當(dāng)泛型參數(shù)是DateTime和Int兩個(gè)值類型時(shí),針對各自類型生成了特有的構(gòu)造函數(shù):
GenericType_1__ctor_mBD3620561730BBF48F1E983A56B62AD1A7487645_gshared和GenericType_1__ctor_mC02B2327528BC3724FACBF36E3CA75FA335AC1B7_gshared。
另外il2cpp總是先生成全共享代碼(參數(shù)是引用類型的泛型類)。其它參數(shù)是值類型的泛型代碼是在托管代碼中有用到時(shí)才會(huì)生成特有類型的代碼,從而減少代碼體量。
另外泛型類中的函數(shù)也是和泛型類性綁定的,不論函數(shù)是否使用泛型類型參數(shù)。如圖3-13中的UseGenericParameter()和DoesNotUseGenericParameter()都生成了類型特有的方法。如圖3-15所示。
(8)P/Invloke封裝
P/Invoke就是托管代碼調(diào)用封裝在DLL中的非托管函數(shù)。雖然IL2CPP會(huì)將托管代碼轉(zhuǎn)換為C++代碼,但I(xiàn)L2CPP針對C#中數(shù)據(jù)類型生成的C++數(shù)據(jù)類型和原生的C++類型會(huì)存在區(qū)別,所有IL2CPP運(yùn)行時(shí)就需要進(jìn)行類型轉(zhuǎn)換操作。
托管代碼中,數(shù)據(jù)類型可以分為兩類:blittable和non-blittable。blittable類型數(shù)據(jù)在托管和原生代碼中內(nèi)存表示一致,如:byte、int、float等;
而non-blittable類型數(shù)據(jù)在托管代碼中和C++原生代碼中內(nèi)存表現(xiàn)不同,如:bool、string、array等。而進(jìn)行類型轉(zhuǎn)換就會(huì)引發(fā)分配新的內(nèi)存。
a、內(nèi)存轉(zhuǎn)換non-blittable類型
如圖3-16所示是生成的C++代碼,其中string類型參數(shù)被轉(zhuǎn)換成char*,產(chǎn)生了新的內(nèi)存分配,并且在函數(shù)執(zhí)行結(jié)束后,調(diào)用il2cpp_codegen_marshal_free釋放char*內(nèi)存。所以non-blittable類型內(nèi)存轉(zhuǎn)換相對于blittable類型的轉(zhuǎn)換是個(gè)耗時(shí)又要有新內(nèi)存開銷的操作。
b、內(nèi)存轉(zhuǎn)換數(shù)組
如果轉(zhuǎn)換的是一個(gè)元素為blittable類型的數(shù)組,只是調(diào)用il2cpp_codegen_marshal_array()返回托管代碼中數(shù)組的首地址。如圖3-19。
如果轉(zhuǎn)換元素為non-bilttable類型的數(shù)組,則需要調(diào)用il2cpp_codegen_marshal_allocate_array()分配一個(gè)新的數(shù)組,并且對數(shù)組中每個(gè)元素做一次內(nèi)存轉(zhuǎn)換。如圖3-21。
4、使用IL2CPP代碼優(yōu)化:
(1)Devirtualization
通過對IL2CPP生成的C++代碼分析,當(dāng)調(diào)用一個(gè)抽象方法時(shí),il2cpp生成的C++代碼都會(huì)執(zhí)行一次虛方法調(diào)用(VirtFuncInvoker),虛方法調(diào)用就會(huì)查詢虛函數(shù)表vtable找到合適的方法進(jìn)行調(diào)用,所以虛方法調(diào)用比直接函數(shù)調(diào)用開銷大。
所以我們應(yīng)盡量避免虛方法調(diào)用,明確直接方法調(diào)用。另外,能明確不需要子類繼承的類或方法使用sealed關(guān)鍵字標(biāo)記,這樣il2cpp生成C++代碼時(shí)可以明確的知道使用哪個(gè)方法,就可以直接調(diào)用對應(yīng)的方法取代虛方法調(diào)用。
以上內(nèi)容翻譯自Unity Blog中來自Josh Peterson的博客(https://blog.unity.com/author/cap-josh)以及Unity官方手冊。