1、为什么 C# 需要”转译”?
C# 是一个托管语言。你写的 C# 代码编译后得到的不是机器码,而是 IL(Intermediate Language,中间语言)。运行时由 .NET Runtime 或者 Mono Runtime 来解释执行或者 JIT(Just-In-Time)编译执行。
在 PC 上,这毫无问题。Windows 自带了 .NET Runtime,或者 Unity 可以内嵌一个 Mono Runtime 在你的游戏里。JIT 编译一下,跑得飞起。
但是 iOS 呢?
苹果的规则很严格:iOS 不允许 JIT 编译。你不能在运行的时候动态生成可执行代码。所以 Mono 的 JIT 模式在 iOS 上直接就被毙了。
这就是 IL2CPP 诞生的原因。
2、Mono 时代:简单,但不够
Unity 最早的后端是 Mono。C# 脚本 → 编译成 IL → Mono Runtime 解释执行。
在早期,Mono 基本够用。但有几个问题慢慢暴露出来:
- iOS 上的 JIT 限制:Mono 在 iOS 只能用 AOT 模式(Ahead-Of-Time,提前编译)。但 Mono 的 AOT 支持并不完善,很多 C# 高级特性(泛型、反射的某些用法)在 AOT 下会出问题
- Mono 版本跟不上:Unity 用的 Mono 版本长期停留在 .NET 2.0/3.5 时代,和 .NET 社区的主流版本差距越来越大
- 性能天花板:Mono 的 JIT 编译器优化不如现代编译器,游戏这种对性能极度敏感的场景,差 10% 可能就是 30fps 和 33fps 的区别
所以 Unity 做了一个重大的技术决策——开发 IL2CPP,把 C# IL 转成 C++,再用平台原生编译器编译。
3、IL2CPP 是怎么工作的?
名字已经透露了一切:IL → To → C++。
它的工作流程分两步:
第一步:IL2CPP.exe(转译器)
IL2CPP 是一个独立的可执行文件(在 Unity 安装目录里)。它做的是:
- 读入所有 C# 脚本编译出来的 IL 程序集(
.dll文件) - 把 IL 代码转译成 C++ 代码
- 同时生成一份 C++ 版本的 GC(垃圾回收器)、线程模型、反射支持代码
这个「转译」的过程是静态的,在构建时完成。不存在运行时 JIT。
第二步:平台编译器
生成的 C++ 代码交给对应平台的原生编译器:
- iOS → Xcode 的 clang
- Android → NDK 的 clang
- Windows → MSVC 或 clang
- macOS → Xcode 的 clang
原生编译器把生成的 C++ 编译成机器码,最终链接成可执行文件。
4、IL2CPP 带来了什么好处?
跨平台统一。不管你发布到哪个平台,C# 代码都先转成 C++,再用该平台的原生编译器编译。这样 Unity 只需要维护一套 IL → C++ 的转译器,而不是为每个平台维护一套 Runtime。
性能提升。C++ 编译器(clang、MSVC)已经发展了二十多年,优化能力极强。同样的逻辑,经过 IL2CPP → C++ → clang -O2 这条链路,通常比 Mono JIT 跑得更快。官方数据说 IL2CPP 比 Mono 快 1.5 到 2 倍。
安全性。IL2CPP 输出的是原生机器码,不像 IL 那样容易被反编译工具(如 ILSpy、dnSpy)直接还原成可读的 C# 源码。对防破解有一点帮助(虽然只是提高门槛,不是绝对安全)。
5、IL2CPP 的代价:构建时间
享受性能提升的代价,就是构建时间暴涨。
Mono 构建的流程是:C# → IL → 打包 Mono Runtime + IL → 出包。IL2CPP 构建多了两道「重」工序:
- IL → C++ 转译(你的 C# 代码越大,生成的 C++ 越多)
- 原生编译(clang 编译几万甚至几十万行生成的 C++ 代码)
两三万行 C# 的项目,IL2CPP 可能生成十几万行 C++。十几万行 C++ 的正常编译时间,你应该心里有数。
更雪上加霜的是,Unity 每次构建都会全量重新生成 C++ 代码,然后全量重新编译。如果原生编译器没有很好的缓存机制(比如 Xcode 的 DerivedData),每次构建都是从零开始。
一些团队的做法是:
- 本地开发用 Mono 后端(构建快,迭代方便)
- CI / 出包用 IL2CPP 后端(构建慢,但出的是正式包)
- 用 cache-server 缓存编译中间产物
6、平台差异:不是所有平台都一样
iOS
- 必须用 IL2CPP(苹果不允许 JIT)
- 构建产物是 Xcode Project,需要 Mac 上用 Xcode 再编译一次
- 纹理压缩强制使用 PVRTC 或 ASTC
- 构建时间最长(因为生成 Xcode Project 后还要再编译一遍)
Android
- 可选 Mono 或 IL2CPP
- IL2CPP 构建需要 NDK
- APK 或 AAB 输出
- 纹理压缩通常用 ETC2 或 ASTC
- 构建时间主要卡在 NDK 编译阶段
Windows / macOS / Linux
- 桌面平台,可选 Mono 或 IL2CPP
- 构建最快(不需要像 iOS 那样再套一层 Xcode 编译)
WebGL
- 只能用 IL2CPP(WebAssembly 的要求)
- 构建产物是 WebAssembly 文件
- C# 代码被转成 C++ 然后编译成 Wasm
- 构建时间很长,WebAssembly 限制了多线程,编译速度慢
7、常见坑点
泛型 + IL2CPP 的组合。Mono 下能跑的泛型代码,在 IL2CPP 下可能会出「ExecutionEngineException」或者直接崩溃。因为 IL2CPP 需要在构建时确定所有用到的泛型实例化,做 AOT 编译。如果你用了大量动态反射 + 泛型,IL2CPP 可能「猜不到」你的实际类型参数。
不过好消息是,Unity 近年做了很多改进——[Preserve] 特性、link.xml 配置、Managed Stripping Level 控制,可以让开发者手动干预 IL2CPP 的裁剪行为。
构建失败不留有效错误信息。IL2CPP 转译或编译失败的时候,错误信息常常淹没在几万行输出里。排查起来非常痛苦。建议用 -logFile 把构建日志输出到文件,再搜索 error 关键字。
增量构建不靠谱。理论上 IL2CPP 支持增量构建——你只改了一点 C# 代码,下次构建应该只重新生成和编译变化的部分。但实践中,很多团队的增量构建跑着跑着就失效了,变成全量重编。原因通常是 Library 目录里的缓存文件被清理或不一致。
8、小结
IL2CPP 是 Unity 为了跨越平台鸿沟而生的技术方案。它的逻辑很清晰:
既然不同平台有不同的编译器和限制,那就把 C# 统一转成 C++——一个几乎所有平台都能编译的语言。
思路是优雅的,代价是构建时间。这种「牺牲构建效率换运行效率」的取舍,在游戏引擎领域是非常经典的 trade-off。
到这里,Unity 构建五篇文章就写完了。我们走过了:
- 构建流程全景(Build 按钮背后的八个阶段)
- AssetBundle 接口演进(第 0 代手写 → 第 1 代标记/代码两种路线 → 第 2 代 SBP → 第 2 代 Addressables)
- Addressables 资源管理系统
- IL2CPP 的跨平台机制(C# → C++ → 机器码)
下一篇开始,我们转战 Unreal——先聊聊 UBT 和 UAT 是什么,.uproject 是怎么变成一个 .exe 的?
每天前进一小步,就是一个新的高度!