IL2CPP深入講解:方法調(diào)用介紹
IL2CPP INTERNALS: METHOD CALLS
作者:JOSH PETERSON
翻譯:Bowie
這里是本系列的第四篇博文王财。在這篇文章里弯菊,我們將看到il2cpp.exe如何為托管代碼中的各種函數(shù)調(diào)用生成C++代碼钠四。我們在這里會著重的分析6種不同類型的函數(shù)調(diào)用:
類實例的成員函數(shù)調(diào)用和類的靜態(tài)函數(shù)調(diào)用驴党。
編譯期生成的代理函數(shù)調(diào)用
虛函數(shù)調(diào)用
C#接口(Interface)函數(shù)調(diào)用
運行期決定的代理函數(shù)調(diào)用
通過反射機制的函數(shù)調(diào)用
對于每種情況,我們主要探討兩點:相應的C++代碼都做了些啥以及這么做的開銷如何誊薄。
和以往的文章一樣履恩,我們這里所討論的代碼,很可能在新的Unity版本中已經(jīng)發(fā)生了變化呢蔫。盡管如此切心,文章所闡述的基本概念是不會變的。而文章中關(guān)于代碼的部分都是屬于實現(xiàn)細節(jié)片吊。
項目設(shè)置
這次我采用的Unity版本是5.0.1p4绽昏。運行環(huán)境為Windows,目標平臺選擇了WebGL俏脊。同樣的全谤,在構(gòu)建設(shè)置中勾選了“Development Player”并且將“Enable Exceptions”選項設(shè)置成“Full”。
我將使用一個在上一篇文章中的C#代碼爷贫,做一點小的修改认然,放入不同類型的調(diào)用方法。代碼以一個接口(Interface)定義和類的定義開始:
Interface Interface { int MethodOnInterface(string question); } class Important : Interface { public int Method(string question) { return 42; } public int MethodOnInterface(string question) { return 42; } public static int StaticMethod(string question) { return 42; } }
接下來是后面代碼要用到的常數(shù)變量和代理類型:
private const string question = "What is the answer to the ultimate question of life, the universe, and everything?"; private delegate int ImportantMethodDelegate(string question);
最后是我們討論的主題:6種不同的函數(shù)調(diào)用的代碼(以及必須要有的啟動函數(shù)漫萄,啟動函數(shù)具體代碼就不放上來了):
private void CallDirectly() { var important = ImportantFactory(); important.Method(question); } private void CallStaticMethodDirectly() { Important.StaticMethod(question); } private void CallViaDelegate() { var important = ImportantFactory(); ImportantMethodDelegate indirect = important.Method; indirect(question); } private void CallViaRuntimeDelegate() { var important = ImportantFactory(); var runtimeDelegate = Delegate.CreateDelegate(typeof (ImportantMethodDelegate), important, "Method"); runtimeDelegate.DynamicInvoke(question); } private void CallViaInterface() { Interface importantViaInterface = new Important(); importantViaInterface.MethodOnInterface(question); } private void CallViaReflection() { var important = ImportantFactory(); var methodInfo = typeof(Important).GetMethod("Method"); methodInfo.Invoke(important, new object[] {question}); } private static Important ImportantFactory() { var important = new Important(); return important; } void Start () {}
有了這些以后卷员,我們就可以開始了。還記得所有生成的C++代碼都會臨時存放在Temp\StagingArea\Data\il2cppOutput目錄下么卷胯?(只要Unity Editor保持打開)別忘了你也可以使用 Ctags 去標注這些代碼子刮,讓閱讀變得更容易。
直接函數(shù)調(diào)用
最簡單(當然也是最快速)調(diào)用函數(shù)的方式窑睁,就是直接調(diào)用挺峡。下面是CallDirectly方法的C++實現(xiàn):
Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo); V_0 = L_0; Important_t1 * L_1 = V_0; NullCheck(L_1); Important_Method_m1(L_1, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_Method_m1_MethodInfo);
代碼的最后一行是實際的函數(shù)調(diào)用。其實沒有什么特別的地方担钮,就是一個普通的C++全局函數(shù)調(diào)用而已橱赠。大家是否還記得“代碼生成之旅”文章中提到的內(nèi)容:il2cpp.exe產(chǎn)生的C++代碼的函數(shù)全部是類C形式的全局函數(shù),這些函數(shù)不是虛函數(shù)也不是屬于任何類的成員函數(shù)箫津。接下來狭姨,直接靜態(tài)函數(shù)的調(diào)用和前面的處理很相似。下面是靜態(tài)函數(shù)CallStaticMethodDirectly的C++代碼:
Important_StaticMethod_m3(NULL /*static, unused*/, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_StaticMethod_m3_MethodInfo);
相比之前苏遥,我們可以說靜態(tài)函數(shù)的代碼處理要簡單的多饼拍,因為我們不需要類的實例,所以我們也不需要創(chuàng)建實例田炭,進行實例檢查的那些個代碼师抄。靜態(tài)函數(shù)的調(diào)用和一般函數(shù)調(diào)用的區(qū)別僅僅在于第一個參數(shù):靜態(tài)函數(shù)的第一個參數(shù)永遠是NULL。
由于這兩類函數(shù)的區(qū)別是如此之小教硫,因此在后面的文章中叨吮,我們只會拿一般函數(shù)調(diào)用來討論辆布。但是這些討論的內(nèi)容同樣適用于靜態(tài)函數(shù)。
編譯期代理函數(shù)調(diào)用
像這種通過代理函數(shù)來進行非直接調(diào)用的稍微復雜點的情況會發(fā)生什么呢茶鉴?CallViaDelegate函數(shù)調(diào)用的C++代碼如下:
// Get the object instance used to call the method. Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo); V_0 = L_0; Important_t1 * L_1 = V_0; // Create the delegate. IntPtr_t L_2 = { &Important_Method_m1_MethodInfo }; ImportantMethodDelegate_t4 * L_3 = (ImportantMethodDelegate_t4 *)il2cpp_codegen_object_new (InitializedTypeInfo(&ImportantMethodDelegate_t4_il2cpp_TypeInfo)); ImportantMethodDelegate__ctor_m4(L_3, L_1, L_2, /*hidden argument*/&ImportantMethodDelegate__ctor_m4_MethodInfo); V_1 = L_3; ImportantMethodDelegate_t4 * L_4 = V_1; // Call the method NullCheck(L_4); VirtFuncInvoker1< int32_t, String_t* >::Invoke(&ImportantMethodDelegate_Invoke_m5_MethodInfo, L_4, (String_t*) &_stringLiteral1);
我加入了一些注釋以標明上面代碼的不同部分锋玲。需要注意的是實際上在C++中調(diào)用的是VirtFuncInvoker1<int32_t, String_t>::Invoke這個函數(shù)。此函數(shù)位于GeneratedVirtualInvokers.h頭文件中涵叮。它不是由我們寫的IL代碼生成的惭蹂,相反的,il2cpp.exe是根據(jù)虛函數(shù)是否有返回值割粮,和虛函數(shù)的參數(shù)個數(shù)來生成這個函數(shù)的剿干。(譯注:VirtFuncInvokerN是表示有N個參數(shù)有返回值的虛函數(shù)調(diào)用,而VirtActionInvokerN 則表示有N個參數(shù)但是沒有返回值的虛函數(shù)調(diào)用穆刻,上面的例子中VirtFuncInvoker1<int32_t, String_t>::Invoke的第一個模板參數(shù)int32_t就是函數(shù)的返回值置尔,而VirtFuncInvoker1中的1表示此函數(shù)還有一個參數(shù),也就是模板參數(shù)中的第二個參數(shù):String_t*氢伟。因此可以推斷VirtFuncInvoker2應該是類似這樣的形式:VirtFuncInvoker2<R, S, T>::Invoke榜轿,其中R是返回值,S朵锣,T是兩個參數(shù))
具體的Invoke函數(shù)看起來是下面這個樣子的:
template <typename R, typename T1> struct VirtFuncInvoker1 { typedef R (*Func)(void*, T1, MethodInfo*); static inline R Invoke (MethodInfo* method, void* obj, T1 p1) { VirtualInvokeData data = il2cpp::vm::Runtime::GetVirtualInvokeData (method, obj); return ((Func)data.methodInfo->method)(data.target, p1, data.methodInfo); } };
libil2cpp中的GetVirtualInvokeData函數(shù)實際上是在一個虛函數(shù)表的結(jié)構(gòu)中尋找對應的虛函數(shù)谬盐。而這個虛函數(shù)表是根據(jù)C#托管代碼建立的。在找到了這個虛函數(shù)后诚些,代碼就直接調(diào)用它飞傀,傳入需要的參數(shù),從而完成了函數(shù)調(diào)用過程诬烹。
你可能會問砸烦,為什么我們不用C++11標準中的可變參數(shù)模板 (譯注:所謂可變參數(shù)模板是諸如template<typename T, typename...Args>,這樣的形式绞吁,后面的...和函數(shù)中的可變參數(shù)...作用是一樣的)來實現(xiàn)這些個VirtFuncInvokerN函數(shù)幢痘?這恰恰是可變參數(shù)模板能解決的問題啊。然而家破,考慮到由il2cpp.exe生成的C++代碼要在各個平臺的C++編譯器中進行編譯颜说,而不是所有的編譯器都支持C++11標準。所以我們再三權(quán)衡汰聋,沒有使用這項技術(shù)门粪。
那么虛函數(shù)調(diào)用又是怎么回事?我們調(diào)用的不是C#類里面的一個普通函數(shù)嗎烹困?大家回想下上面的代碼:我們實際上是通過一個代理方法來調(diào)用類中的函數(shù)的玄妈。再來看看上面的C++代碼,實際的函數(shù)調(diào)用是通過傳遞一個MethodInfo*結(jié)構(gòu)(函數(shù)元信息結(jié)構(gòu)):ImportantMethodDelegate_Invoke_m5_MethodInfo作為參數(shù)來完成的。再進一步看ImportantMethodDelegate_Invoke_m5_MethodInfo中的內(nèi)容措近,會發(fā)現(xiàn)它實際上調(diào)用的是C#代碼中ImportantMethodDelegate類型的Invoke函數(shù)(譯注:也就是C#代理函數(shù)類型的Invoke函數(shù))。而這個Invoke函數(shù)是個虛函數(shù)女淑,所以最終我們也是以虛函數(shù)的方式調(diào)用的瞭郑。
Wow,這夠我們消化一陣子的了鸭你。在C#中的一點小小的改變屈张,在我們的C++代碼中從簡單的函數(shù)調(diào)用變成了一系列的復雜函數(shù)調(diào)用,這中間還牽扯到了查找虛函數(shù)表袱巨。顯然通過代理的方法調(diào)用比直接函數(shù)調(diào)用更耗時阁谆。
還有一點需要注意的是在代理方法調(diào)用處理時候使用的這個查找虛函數(shù)表的操作,也同樣適用于虛函數(shù)調(diào)用愉老。
接口方法調(diào)用
在C#中通過接口方法調(diào)用當然也是可以的场绿。在C++代碼實現(xiàn)中和虛函數(shù)的處理方式差不多:
Important_t1 * L_0 = (Important_t1 *)il2cpp_codegen_object_new (InitializedTypeInfo(&Important_t1_il2cpp_TypeInfo)); Important__ctor_m0(L_0, /*hidden argument*/&Important__ctor_m0_MethodInfo); V_0 = L_0; Object_t * L_1 = V_0; NullCheck(L_1); InterfaceFuncInvoker1< int32_t, String_t* >::Invoke(&Interface_MethodOnInterface_m22_MethodInfo, L_1, (String_t*) &_stringLiteral1);
實際上的函數(shù)調(diào)用是通過InterfaceFuncInvoker1::Invoke來完成的。這個函數(shù)存在于GeneratedInterfaceInvokers.h頭文件中嫉入。就像上面提到過的VirtFuncInvoker1類焰盗,InterfaceFuncInvoker1類也是通過il2cpp::vm::Runtime::GetInterfaceInvokeData查詢虛函數(shù)表來確定實際調(diào)用的函數(shù)的。
那為什么接口的方法調(diào)用和虛函數(shù)的調(diào)用在libil2cpp庫中是不同的API呢咒林?那是因為在接口方法調(diào)用中熬拒,除了方法本身的元信息,函數(shù)參數(shù)之外垫竞,我們還需要接口本身(在上面的例子中就是L_1)在虛函數(shù)表中接口的方法是被放在一個特定的偏移上的澎粟。因此il2cpp.exe需要接口的信息去計算出被調(diào)用的函數(shù)到底是哪一個。
從代碼的最后一行可以看出欢瞪,調(diào)用接口的方法和調(diào)用虛函數(shù)的開銷在IL2CPP中是一樣的活烙。
運行期決定的代理方法調(diào)用
使用代理的另一個方法是在運行時由Delegate.CreateDelegate動態(tài)的創(chuàng)建代理實例。這個過程實際上和編譯期的代理很像遣鼓,只是多了一些運行時的處理瓣颅。為了代碼的靈活性,我們總是要付出些代價的譬正。下面是實際的代碼:
// Get the object instance used to call the method. Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo); V_0 = L_0; // Create the delegate. IL2CPP_RUNTIME_CLASS_INIT(InitializedTypeInfo(&Type_t_il2cpp_TypeInfo)); Type_t * L_1 = Type_GetTypeFromHandle_m19(NULL /*static, unused*/, LoadTypeToken(&ImportantMethodDelegate_t4_0_0_0), /*hidden argument*/&Type_GetTypeFromHandle_m19_MethodInfo); Important_t1 * L_2 = V_0; Delegate_t12 * L_3 = Delegate_CreateDelegate_m20(NULL /*static, unused*/, L_1, L_2, (String_t*) &_stringLiteral2, /*hidden argument*/&Delegate_CreateDelegate_m20_MethodInfo); V_1 = L_3; Delegate_t12 * L_4 = V_1; // Call the method ObjectU5BU5D_t9* L_5 = ((ObjectU5BU5D_t9*)SZArrayNew(ObjectU5BU5D_t9_il2cpp_TypeInfo_var, 1)); NullCheck(L_5); IL2CPP_ARRAY_BOUNDS_CHECK(L_5, 0); ArrayElementTypeCheck (L_5, (String_t*) &_stringLiteral1); *((Object_t **)(Object_t **)SZArrayLdElema(L_5, 0)) = (Object_t *)(String_t*) &_stringLiteral1; NullCheck(L_4); Delegate_DynamicInvoke_m21(L_4, L_5, /*hidden argument*/&Delegate_DynamicInvoke_m21_MethodInfo);
首先我們使用了一些代碼來創(chuàng)建代理這個實例宫补,隨后處理函數(shù)調(diào)用的代碼也不少。在后面的過程中我們先創(chuàng)建了一個數(shù)組用來存放被調(diào)用函數(shù)的參數(shù)曾我。然后調(diào)用代理實例中的DynamicInvoke方法粉怕。如果我們更深入的研究下DynamicInvoke方法,會發(fā)現(xiàn)它實際上在內(nèi)部調(diào)用了VirtFuncInvoker1::Invoke函數(shù)抒巢,就如同編譯期代理所做的那樣贫贝。所以從代碼執(zhí)行量上來說,運行時代理方法比靜態(tài)編譯代理方法多了一個函數(shù)創(chuàng)建,比且還多了一次虛函數(shù)表的查找稚晚。
通過反射機制進行方法調(diào)用
毫無疑問的崇堵,通過反射來調(diào)用函數(shù)開銷是最大的。下面我們來看看具體的CallViaReflection函數(shù)所生成的C++代碼:
// Get the object instance used to call the method. Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo); V_0 = L_0; // Get the method metadata from the type via reflection. IL2CPP_RUNTIME_CLASS_INIT(InitializedTypeInfo(&Type_t_il2cpp_TypeInfo)); Type_t * L_1 = Type_GetTypeFromHandle_m19(NULL /*static, unused*/, LoadTypeToken(&Important_t1_0_0_0), /*hidden argument*/&Type_GetTypeFromHandle_m19_MethodInfo); NullCheck(L_1); MethodInfo_t * L_2 = (MethodInfo_t *)VirtFuncInvoker1< MethodInfo_t *, String_t* >::Invoke(&Type_GetMethod_m23_MethodInfo, L_1, (String_t*) &_stringLiteral2); V_1 = L_2; MethodInfo_t * L_3 = V_1; // Call the method. Important_t1 * L_4 = V_0; ObjectU5BU5D_t9* L_5 = ((ObjectU5BU5D_t9*)SZArrayNew(ObjectU5BU5D_t9_il2cpp_TypeInfo_var, 1)); NullCheck(L_5); IL2CPP_ARRAY_BOUNDS_CHECK(L_5, 0); ArrayElementTypeCheck (L_5, (String_t*) &_stringLiteral1); *((Object_t **)(Object_t **)SZArrayLdElema(L_5, 0)) = (Object_t *)(String_t*) &_stringLiteral1; NullCheck(L_3); VirtFuncInvoker2< Object_t *, Object_t *, ObjectU5BU5D_t9* >::Invoke(&MethodBase_Invoke_m24_MethodInfo, L_3, L_4, L_5);
就和運行時代理方法調(diào)用一樣客燕,我們需要用額外的代碼創(chuàng)建函數(shù)參數(shù)數(shù)組鸳劳。然后還需要調(diào)用一個MethodBase::Invoke (實際上是MethodBase_Invoke_m24函數(shù))虛函數(shù),由這個函數(shù)調(diào)用另外一個虛函數(shù)也搓,在能最終得到實際的函數(shù)調(diào)用赏廓!
總結(jié)
雖然Unity沒有針對C++函數(shù)調(diào)用的性能分析器,但是我們可以從C++的源碼中看出不同類型方法調(diào)用的不同復雜程度的實現(xiàn)傍妒。如何可能幔摸,請盡量避免使用運行時代理方法和反射機制方法的調(diào)用。當然颤练,想要提高運行效率還是要在項目的早期階段就使用性能分析器進行診斷既忆。
我們也在一直想辦法優(yōu)化il2cpp.exe產(chǎn)生的代碼。因此再次強調(diào)嗦玖,這篇文章中所產(chǎn)生的C++代碼或許會在以后的Unity版本中發(fā)生變化尿贫。
下篇文章我們將更進一步的深入到函數(shù)中,看看我們是如何共享方法簡化C++代碼并減小最終可執(zhí)行文件的尺寸的踏揣。