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ī)平臺(tái)編譯器編譯生成的 C++ 代碼和 IL2CPP 的運(yùn)行時(shí)部分荒典。
e宗挥、將代碼鏈接到可執(zhí)行文件或 DLL,具體取決于目標(biāo)平臺(tái)种蝶。
(2)運(yùn)行時(shí)庫(kù)(libil2cpp)
il2cpp頭文件和源碼位置:
Unity安裝目錄下:Unity\Editor\Data\il2cpp\libil2cpp
2契耿、IL2CPP特點(diǎn):
(1)針對(duì)C#中計(jì)算密集型代碼性能相比Mono提高很多
(2)只支持AOT編譯
(3)相對(duì)于Mono,構(gòu)建時(shí)間更長(zhǎng)螃征,生成代碼量更大
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)用對(duì)應(yīng)的get方法交排。
(2)普通成員函數(shù):
(3)靜態(tài)方法
如上面幾個(gè)圖所示划滋,所有的函數(shù)都被聲明成了IL2CPP_EXTERN_C也就是extern “C”類型,這樣一來(lái)埃篓,在需要的時(shí)候就可以騙過(guò)C++編譯器讓其認(rèn)為所有這些函數(shù)都是一個(gè)類型处坪。
所有的函數(shù)都不是成員函數(shù)。函數(shù)的第一個(gè)參數(shù)永遠(yuǎn)都是“this”指針架专。對(duì)于托管代碼中的靜態(tài)函數(shù)同窘,IL2CPP會(huì)忽略這個(gè)參數(shù)也就相當(dāng)于傳遞NULL作為第一個(gè)參數(shù)的值。這么做的好處是可以讓il2cpp.exe轉(zhuǎn)換代碼的邏輯更加簡(jiǎn)單并且讓代理函數(shù)的處理變得更加容易部脚。所有的函數(shù)還有一個(gè)額外的RuntimeMethod*參數(shù)用來(lái)描述函數(shù)的元信息塞椎。這些元信息是虛函數(shù)調(diào)用的關(guān)鍵。Mono使用和特定平臺(tái)相關(guān)的方法來(lái)傳遞這些元信息睛低。而IL2CPP出于可移植方面的考慮案狠,并沒(méi)有使用這些和平臺(tái)相關(guān)的特定代碼。在this指針和RuntimeMethod*之間的就是函數(shù)實(shí)際用到的參數(shù)钱雷。
托管代碼中的類型會(huì)被加上“_t”的后綴骂铁,函數(shù)則是加上“_m”后綴和一個(gè)唯一的MD5碼來(lái)避免名字的重復(fù)。
(4)異常處理
托管代碼中的異常都會(huì)被il2cpp轉(zhuǎn)換成C++的異常罩抗。如圖3-11拉庵,所有托管異常都被封裝在C++中的IL2CppExcetionWrapper類型,當(dāng)C++代碼捕獲異常后套蒂,會(huì)解析獲取對(duì)應(yīng)的托管異常钞支,通過(guò)il2cpp_codegen_class_is_assignable_from()判斷該異常是否要捕獲的異常,如果是則跳轉(zhuǎn)至Catch代碼塊執(zhí)行操刀,否則拋出一個(gè)C++異常烁挟。
(5)goto
如圖3-11所示,我們發(fā)現(xiàn)生成的C++代碼中包含goto語(yǔ)句骨坑。這是因?yàn)镮L中沒(méi)有while撼嗓、do和for循環(huán)以及if/else結(jié)構(gòu),它們都是使用標(biāo)簽欢唾、相等goto與條件goto語(yǔ)句實(shí)現(xiàn)的且警。所以il2cpp在處理IL代碼時(shí),也直接使用標(biāo)簽+goto的方式實(shí)現(xiàn)礁遣。
(6)il2cpp.exe不針對(duì)每一個(gè)類中的函數(shù)生成單獨(dú)的一個(gè)cpp文件斑芜,而是將很多類的函數(shù)放在一個(gè).cpp文件。如上圖祟霍,Assetbly-CSharp中所有的函數(shù)被生成在5個(gè).cpp文件中杏头。il2cpp之所以這么做的原因是C++編譯器在編譯相同代碼量的情況下盈包,處理大量的文件的用時(shí)比集中處理少量文件長(zhǎng)很多。
(7)泛型共享
il2cpp針對(duì)泛型參數(shù)是引用類型還是值類型生成不同的共享代碼大州。
對(duì)于引用類型的泛型共享续语,由于所有托管代碼中的引用類型都繼承自System.Object垂谢,轉(zhuǎn)成C++代碼后都能用RuntimeObject *指針表示厦画。所以所有引用類型都可以共享一份代碼實(shí)現(xiàn)。
而對(duì)于值類型滥朱,泛型T的大小不同根暑,只能針對(duì)不同的值類型生成特有的代碼。
如圖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í)缰犁,針對(duì)各自類型生成了特有的構(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針對(duì)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)換相對(duì)于blittable類型的轉(zhuǎn)換是個(gè)耗時(shí)又要有新內(nèi)存開(kā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ù)組,并且對(duì)數(shù)組中每個(gè)元素做一次內(nèi)存轉(zhuǎn)換挟纱。如圖3-21羞酗。
4、使用IL2CPP代碼優(yōu)化:
(1)Devirtualization
通過(guò)對(duì)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)用開(kāi)銷大欺嗤。
所以我們應(yīng)盡量避免虛方法調(diào)用参萄,明確直接方法調(diào)用。另外剂府,能明確不需要子類繼承的類或方法使用sealed關(guān)鍵字標(biāo)記拧揽,這樣il2cpp生成C++代碼時(shí)可以明確的知道使用哪個(gè)方法,就可以直接調(diào)用對(duì)應(yīng)的方法取代虛方法調(diào)用腺占。
以上內(nèi)容翻譯自Unity Blog中來(lái)自Josh Peterson的博客(https://blog.unity.com/author/cap-josh)以及Unity官方手冊(cè)淤袜。