IL2CPP原理简述

代码总览

在Unity打包过程中IL2CPP会生成il2cpp代码。生成的目录是Temp/StagingArea/Il2Cpp/il2cppOutput。因为是在Temp目录,Unity关闭时会移除它。可以复制出来研究。以我现在工作的项目来说,有616个C++文件,总共1.07G大小。生成的文件总体概述如下:

Bulk_Assembly-CSharp_{递增数字}.cpp这些是游戏内Assembly-CSharp.dll中类型对应生成,主要是逻辑代码。

Bulk_Assembly-CSharp-firstpass_{递增数字}.cpp这些是游戏内Assembly-CSharp-firstpass.dll中类型对应生成,是Plugins中的类型。

Bulk_Generics_{递增数字}.cpp是泛型特化对应的生成代码

Bulk_mscorlib_{递增数字}.cpp mscorlib核心库对应的生成代码

Bulk_System.Xml_{递增数字}.cpp是System.Xml命名空间对应的生成代码,这样的还有不少。

GenericMethods{递增数字}.cpp泛型方法特化对应的生成代码。

Il2CppCompilerCalculateTypeValues_{递增数字}Table.cpp包含泛型属性的类型对应的生成代码。

{递增数字}这个在后面将一再看到,是一种避免冲突的好办法!!!

编译过程见拙著IL2CPP编译过程从其中发现还依赖于Unity本身提供的一些库(都在/Applications/Unity/Unity.app/Contents/il2cpp/目录下面):

external/boehmgc就是boehm垃圾回收器

libil2cpp/icalls/下面是些C#都有的扩展类比如:CurrentSystemTimeZone RuntimeFieldHandle

libil2cpp/vm下面是虚拟机代码

libil2cpp/os下面是操作系统对应扩展代码


代码分析

全部函数方法(包括成员函数)都是全局函数。 _m{递增数字}来确保不会重名。第一个参数是实例指针,如果是静态函数就对应NULL。

如果是类型通过附加 _t{递增数字}来确保不会重名。从下面可以看到新版生成的代码已经不是附加数字了,是一种类似hash值的东西。


// UnityEngine.Vector3 AkAmbientLargeModePositioner::get_Up()

extern "C" IL2CPP_METHOD_ATTR Vector3_tDCF05E21F632FE2BA260C06E0D10CA81513E6720  AkAmbientLargeModePositioner_get_Up_m4173F97E4E545A66EDF0297A0AF41E0AA9A29AA8 (AkAmbientLargeModePositioner_tAA1DE4C2E8BB1AD248413B957B5EB537DB58ED5E * __this, const RuntimeMethod* method)


从上面可以看到生成的函数上面通过注释标识了原本的函数,这样方便分析。最末尾加上一个参数MethodInfo* 传递metadata用于虚函数调用。mono用的是平台相关的trampolines来传递。cpp为了移植性就改换成这种方式。extern "C"避免以C++的方式处理函数名。如下分析一段生成代码:


V_2 = 0;

gotoIL_00cc;

}

IL_00af:

{

ObjectU5BU5D_t4* L_19 = ((ObjectU5BU5D_t4*)SZArrayNew(ObjectU5BU5D_t4_il2cpp_TypeInfo_var, 1));

int32_t L_20 = V_2;

Object_t * L_21 =Box(InitializedTypeInfo(&Int32_t5_il2cpp_TypeInfo), &L_20);

NullCheck(L_19);

IL2CPP_ARRAY_BOUNDS_CHECK(L_19, 0);

ArrayElementTypeCheck (L_19, L_21);

}

IL_00cc:

{

if ((((int32_t)V_2) < ((int32_t)3)))

{

goto IL_00af;

}


从上面发现C++代码是从IL生成的,而不是从AST语法分析产生的,比较啰嗦。循环是由goto语句产生的(其实生成的语句中包含许多goto)。还有3个运行时检查NullCheck() IL2CPP_ARRAY_BOUNDS_CHECK() ArrayElementTypeCheck ()。

函数调用成本


IL中函数调用有两种方式调用:call和callvirt。call一般是以非虚的方式来调用函数的,callvirt是以已多态的方式来调用函数的。callvirt对应的生成代码如下(这些生成的代码一般在文件头部。名字含有Func的有返回值,名字含有Action的无返回值,根据参数个数名字末尾的数字也不同)

template <typename R>

struct VirtFuncInvoker0

{

typedef R (*Func)(void*, const RuntimeMethod*);

static inline R Invoke (Il2CppMethodSlot slot, RuntimeObject* obj)

{

const VirtualInvokeData& invokeData = il2cpp_codegen_get_virtual_invoke_data(slot, obj);// 查找

return ((Func)invokeData.methodPtr)(obj, invokeData.method);

}

};


struct VirtActionInvoker0

{

typedef void (*Action)(void*, const RuntimeMethod*);

static inline void Invoke (Il2CppMethodSlot slot, RuntimeObject* obj)

{

const VirtualInvokeData& invokeData = il2cpp_codegen_get_virtual_invoke_data(slot, obj);// 查找

((Action)invokeData.methodPtr)(obj, invokeData.method);

}

};

之所以采用这种方式而不使用变参数模板(Vardic Template)是因为为了兼容老的编译器


1. 成员函数和静态函数直接调用(差别就是静态函数的第一个参数是NULL)成本最低。

2. 编译时delegate


// 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);

上面的消耗主要是创建delegate,VirtFuncInvoker1里面查找然后调用。


3. interface调用


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/*interface必须*/, (String_t*) &_stringLiteral1); // 是因为接口函数在虚函数表中有一个统一偏移。所以调用虚函数和调用接口函数有一样的负载。InterfaceFuncInvoker本身也和VirtFuncInvoker类似,代码如下:

template <typename R>

struct InterfaceFuncInvoker0

{

typedef R (*Func)(void*, const RuntimeMethod*);

static inline R Invoke (Il2CppMethodSlot slot, RuntimeClass* declaringInterface, RuntimeObject* obj)

{

const VirtualInvokeData& invokeData = il2cpp_codegen_get_interface_invoke_data(slot, obj, declaringInterface);

return ((Func)invokeData.methodPtr)(obj, invokeData.method);

}

};


4. 运行时delegate。代码更多就不在这贴了,整体步骤是: 获取实例,获取delegate类型,创建delegate,创建参数数组,调用Delegate_DynamicInvoke。在Delegate_DynamicInvoke内部调用的VirtFuncInvoker。

5.运行时delegate,整体步骤是: 获取实例,获取delegate类型,使用字符串参数调用VirtFuncInvoker获取函数,创建参数数组,调用VirtFuncInvoker。由此可见这种调用的成本是最大的。


Generic-Sharing

在C#层的泛型怎么让它的生成代码量最小呢?IL2CPP提供了generic-sharing。支持的类型包括引用类型(string,object和自定义类型)以及整型和枚举。无法为值类型提供generic-sharing,因为它们的占用内存不一样。这能实现是依赖于C#的引用类型基类System.Object在C++层有一个Object_t对应物


class GenericType<T> {

public T UsesGenericParameter(T value) {

return value;

}

会生成如下的 fully shared type的方法

extern "C"Object_t *GenericType_1_UsesGenericParameter_m10449_gshared (GenericType_1_t2159 * __this,Object_t *___value, MethodInfo* method)

{

{

Object_t * L_0 = ___value;

return L_0;

}

}



Marshal

因为类型和函数在C++和C#中有不同表示,所以类型分blittable和non-blittable。如果是blittable的,表示两端(C#和C++)有相同的表示[可以直接穿透]这包括byte,int,flat。non-blittable的就有不同的表示,这包括bool, string, array-types.这就需要转化了,会带来内存损耗。c#为了引用本地代码的函数,需要extern和DllImport属性。需要生成胶水代码。步骤如下:

为函数指针定义typedef

通过名字解析获取到函数的指针

把参数从从托管代码表示方式转化为本地代码表示方式

调用函数

把返回值从本地表示方式转化为托管表示方式

out和ref参数也要这样处理


[DllImport("__Internal")]

private extern static int Increment(int value);     // c#这样声明

extern "C" {int32_t DEFAULT_CALL Increment(int32_t);} 这个在C++层

extern "C" int32_t HelloWorld_Increment_m3 (Object_t * __this /* static, unused */, int32_t ___value, const MethodInfo* method)

{函数内会有static指针查询保存C++层的Increment,然后调用}


对于non-blittable类型,比如string。在il2cpp中表示为2字节字符的数组编码方式为UTF-16前缀是个4字节长度的值表示字符串长度这与char*和wchar_t*类型都不一样,需要一系列转化,il2cpp_codegen_marshal_string会有内存分配与拷贝。如果参数是引用传递,native代码传入的是变量指针。会在函数体内生成一个局部同类型变量,拷贝进去函数调用再拷贝出来。如果是non-blittable类型作为参数,就需要为这参数生成对应的marshaled类型,还需要专门的清理函数来清理分配的内存。比如int数组,因为int是blittable的,所以il2cpp_codegen_marshal_array函数直接返回的是托管数组内存指针。如果是non-blittable的数组,则要为它们分配内存并逐个拷贝,最后还要清理释放。


垃圾收集

当前用的是Boehm-Demers-Weiser垃圾回收算法,并不是分代垃圾回收算法(以后将使用分代垃圾回收器CoreCLR)。Boehm垃圾回收算法由root判断可达性,如果不可达就判定为垃圾,等待回收。
可以作为root的的变量包括:栈上的局部变量,静态变量,GCHandle对象。托管代码中创建一个线程,这个线程就会作为一个gc的root(线程栈的局部变量变成root)。创建函数可能是il2cpp_gc_register_thread。当线程退出时il2cpp_gc_unregister_thread告知GC不用再将它们作为root。这样c++端的对应类型的实例的占用内存就可以回收了。还有类的静态字段并没有直接放在c++类里面,而是另外创建结构。这是为了控制内存布局,并且方便与GC系统协作。


struct  HelloWorld_t2  : public MonoBehaviour_t3

{

};

struct HelloWorld_t2_StaticFields{

// AnyClass HelloWorld::staticAnyClass

AnyClass_t1 * ___staticAnyClass_2;

};


第一次初始化类型时也会引起GC系统为这个HelloWorld_t2_StaticFields类型分配内存,这样内存就由GC系统管理了,作为root。方法是il2cpp_gc_alloc_fixed。

对于从托管内存中传递一个指针到本地代码,由本地代码来获得它的所有权并能够被gc系统处理。这需要在托管代码中的GCHandle

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容