原理:
IL2CPP 的核心流程
IL2CPP 的工作流程可分为以下步骤:
-
C# 代码编译为 IL
Unity 项目中的 C# 脚本首先被编译为 .NET 中间语言(IL)(类似 Java 的字节码),存储在程序集文件(如.dll
)中。 -
IL → C++ 代码转换
IL2CPP 工具链读取这些 IL 代码,将其转换为 C++ 源代码(后置过程)这一步的关键在于:-
直接翻译逻辑:将 IL 指令逐条映射为等效的 C++ 代码。
-
元数据保留:保留类型、方法、字段等元数据,用于运行时反射等功能。
-
垃圾回收集成:生成与 Unity 的垃圾回收器(如 Boehm GC)兼容的代码。
-
-
C++ 代码编译为原生机器码
生成的 C++ 代码会被平台本地编译器(如 iOS 的 Xcode/Clang、Android 的 NDK/GCC)编译为 目标平台的原生二进制文件(如 ARM 或 x86 机器码)。 -
运行时执行
最终生成的机器码直接由 CPU 执行,无需虚拟机或 JIT 编译,性能接近原生 C++ 程序。
与Mono的对比
1. Mono 运行时的核心流程
(1) C# 代码编译为 IL 中间语言
-
Unity 中的 C# 脚本会被编译为 .NET 中间语言(IL),存储在程序集文件(如
.dll
)中。 -
此步骤与 IL2CPP 的初始阶段相同。
(2) 运行时编译与执行
Mono 运行时根据目标平台的能力,选择以下两种模式之一:
-
JIT(Just-In-Time)编译(支持动态生成代码的平台,如 PC、Android):
-
流程:在运行时将 IL 代码 动态编译为机器码,由 CPU 直接执行。
-
优点:支持动态代码生成(如
System.Reflection.Emit
),启动速度快。 -
缺点:运行时编译有性能开销,且某些平台(如 iOS)禁止 JIT。
-
-
AOT(Ahead-Of-Time)编译(受限制的平台,如 iOS):
-
流程:在构建阶段将 部分 IL 代码预先编译为机器码,剩余代码在运行时由轻量解释器处理。
-
优点:绕过平台对 JIT 的限制(如 iOS 的代码签名要求)。
-
缺点:无法编译所有 IL 代码(如泛型、反射生成的代码),可能导致运行时异常。
-
2. Mono 与 IL2CPP 的对比
特性 | Mono (JIT/AOT) | IL2CPP |
---|---|---|
编译方式 | JIT(动态)或 AOT(部分静态) | 完全 AOT(静态生成 C++ → 机器码) |
性能 | 较低(JIT 运行时开销) | 较高(无运行时编译开销) |
iOS 支持 | 受限(AOT 不完整) | 完全支持 |
代码体积 | 较小 | 较大(C++ 代码膨胀) |
动态代码生成 | 支持(JIT 平台) | 不支持 |
构建时间 | 较短 | 较长 |
调试体验 | 直接调试 C# 源码 | 需结合生成的 C++ 代码 |
3. Mono 的局限性
(1) iOS 平台的 AOT 缺陷
-
泛型实例化问题:AOT 只能为 提前已知的泛型类型组合 生成代码。例如:
// 若未在 AOT 阶段预生成 List<int> 和 List<string> 的代码: var list1 = new List<int>(); // 可能正常运行(常见类型已预生成) var list2 = new List<MyClass>(); // 可能崩溃!
-
反射限制:动态加载类型或方法可能失败。
(2) 性能瓶颈
-
JIT 开销:首次执行方法时的编译延迟。
-
内存占用:需同时存储 IL 和机器码。
(3) 安全性
-
JIT 的动态代码生成可能被恶意利用(如 iOS 禁止 JIT 的部分安全考虑)。
(4) Mono 在 iOS 的 AOT 工作流程
-
构建阶段:
-
Unity 调用 Mono 的 AOT 编译器,将部分 IL 编译为 iOS 的 ARM 机器码。
-
生成一个静态库(如
libiPhone-arm.a
)和剩余 IL 代码。
-
-
运行时:
-
预编译的机器码直接执行。
-
未预编译的代码由 Mono 解释器执行(性能极低)。
-
4. 为什么 Unity 需要 IL2CPP?
-
解决 Mono AOT 的缺陷:IL2CPP 通过完全静态编译,规避了泛型和反射的运行时问题。
-
提升性能:C++ 编译器优化能力远超 Mono 的 AOT。
-
遵守平台规则:iOS 等平台禁止动态代码生成(JIT),而 IL2CPP 的完全静态编译符合要求。
5. 总结
-
仅使用 Mono:依赖 JIT 或受限的 AOT 编译,适合对动态代码生成有需求的场景(如 PC 平台),但在 iOS 等受限制平台上面临兼容性和性能问题。
-
IL2CPP 是 Unity 为弥补 Mono 缺陷而引入的解决方案,通过静态编译为 C++ 代码,实现高性能和跨平台一致性。两者适用于不同场景,开发者需根据目标平台和需求权衡选择。
Unity如何构建
要在项目中使用 IL2CPP, 打开 Build Settings. 选择你要发布的平台, 点击 Player Settings 打开 Player settings属性面板
在使用IL2CPP脚本后端时,可以控制il2cpp.exe生成c++代码的方式。特别地,也可以使用下列中的c#属性用来启用或禁用runtime check。
Option | Description | Default |
---|---|---|
Null checks | 如果这个打开(enable),则通过IL2CPP生成的代码会进行null检查,并抛出托管空引用异常 managed NullReferenceException exceptions. 如果此选项被禁用,null检查不会发送到生成的c++代码中。 对于某些项目,禁用此选项可以提高运行时性能。 但是,对生成代码中的空值的任何访问都不会被检查,并可能导致不正确的行为 ,通常,在空值被解除引用后,游戏很快就会崩溃, 请小心禁用此选项 | Enabled |
Array bounds checks | 如果启用此选项,由IL2CPP生成的c++代码将检查数组边界是否越界,并在必要时抛出托管的IndexOutOfRangeException异常,如果禁用此选项生成的c++代码中则不会进行数组边界检查。 同上 | Enabled |
Divide by zero checks | 除以0检查,同上,一般在写代码的时候,就会注意这一点 | Disable |
在c#代码中,如果要开启、禁用以上option,要使用IL2CppSetOptions属性。要使用这个属性,要把Il2CppSetOptionsAttribute.cs脚本拷贝到你的asset文件夹下,在editor->data(Windows上的Data\il2cpp, OS X上的Contents/Frameworks/il2cpp)中
比如:下面没有开启nullcheck检查
[Il2CppSetOption(Option.NullChecks, false)]
public static string MethodWithNullChecksDisabled()
{
var tmp = new object();
return tmp.ToString();
}
可以将Il2CppSetOptions属性应用于类型、方法和属性,
[Il2CppSetOption(Option.NullChecks, false)]
public class TypeWithNullChecksDisabled
{
public static string AnyMethod()
{
// Null checks will be disabled in this method.
var tmp = new object();
return tmp.ToString();
}
[Il2CppSetOption(Option.NullChecks, true)]
public static string MethodWithNullChecksEnabled()
{
// Null checks will be enabled in this method.
var tmp = new object();
return tmp.ToString();
}
}
public class SomeType
{
[Il2CppSetOption(Option.NullChecks, false)]
public string PropertyWithNullChecksDisabled
{
get
{
// Null checks will be disabled here.
var tmp = new object();
return tmp.ToString();
}
set
{
// Null checks will be disabled here.
value.ToString();
}
}
public string PropertyWithNullChecksDisabledOnGetterOnly
{
[Il2CppSetOption(Option.NullChecks, false)]
get
{
// Null checks will be disabled here.
var tmp = new object();
return tmp.ToString();
}
set
{
// Null checks will be enabled here.
value.ToString();
}
}
}
How IL2CPP works
在使用IL2CPP发布时, Unity自动执行以下步骤:
-
编译unity的脚本称常规的.net dll,也就是托管程序集
-
应用托管字节码剥离。这一步大大减少发布游戏的大小Applies managed bytecode stripping. This step significantly reduces the size of a built game.
-
将所有托管程序集转换为标准c++代码
-
使用本机平台编译器编译生成的c++代码和IL2CPP的运行时部分。.
-
根据您的目标平台,将代码链接到可执行文件或DLL中。
上面这张图很到位
Optimizing IL2CPP build times
当使用IL2CPP构建项目时,项目构建时间可能要长得多。然而,有几种方法可以显著减少构建时间:
使用incremental building
当使用 incremental building时,C++ 编译器仅编译相对于上一次发布改变的文件,没有改变的不编译,注意,发布的文件夹要和上一次发布的文件夹是一致的,也就是发布到同一个文件夹下
Exclude project and target build folders from anti-malware software scans
关闭杀毒软件,比如360或者Windows defender,这个会减少50-66%的时间
Store your project and target build folder on a Solid State Drive (SSD)
固态硬盘(ssd)的读/写速度比传统硬盘驱动器(HDD)更快。将IL代码转换为c++并编译它涉及到大量的读/写操作。更快的存储设备加速了这一过程。
Managed stack traces with IL2CPP
当托管代码中发生异常时, stack trace 会告诉你发生错误的地方. 然而它并不总是出现,这时候,就需要配置的il2cpp的错误堆栈, 下面分别是c#自带的和c++配置的
Debug builds
当使用 debug 模式构建时,会打印出错误的堆栈信息
Release builds
当使用 release 模式构建时,IL2CPP 可能会产生缺少一个或多个托管方法的调用堆栈。这是因为C++编译器内联了缺失的方法。方法内联通常对运行时的性能有好处,但它可能使调用堆栈更难以理解。