使用Unity开发游戏 你需要深入了解一下IL2CPP

2016年08月29日 17:51 0 点赞 0 评论 更新于 2025-11-21 20:20
使用Unity开发游戏 你需要深入了解一下IL2CPP

大约一年前,我们曾发布一篇博客,探讨Unity中脚本的未来发展方向。在那篇博客里,我们介绍了全新的IL2CPP后端,并承诺它将为Unity带来更高效、更适配各平台的虚拟机。2015年1月,我们正式发布了首个支持IL2CPP的平台:iOS 64 - bit。随着Unity 5的发布,又新增了一个支持IL2CPP的平台:WebGL。

得益于社区用户提供的大量宝贵反馈,我们在后续时间里依据这些反馈更新IL2CPP,并发布补丁版本,持续改进IL2CPP的编译器和运行时库。

我们并未停止改进IL2CPP的步伐。在当前阶段,我们认为有必要抽出时间,向大家介绍IL2CPP的内部工作机制。接下来几个月,我们计划开展一个IL2CPP深入讲解系列,讨论以下话题(可能还会有其他未列出的话题):

  1. 基础 - 工具链和命令行参数(本篇博文)
  2. IL2CPP生成代码介绍
  3. IL2CPP生成代码调试小窍门
  4. 方法调用介绍(一般方法调用和虚方法调用等)
  5. 通用代码共享的实现
  6. P/invoke(Platform Invocation Service)对于类型(types)和方法(methods)的封装
  7. 垃圾回收器的集成
  8. 测试框架(Testing frameworks)及其使用

在这个系列讨论中,我们会涉及一些未来可能会改动的IL2CPP实现细节。不过,我们希望通过这些讨论,能为大家提供有用且有趣的信息。

什么是IL2CPP?

从技术层面讲,IL2CPP包含两部分:一个进行预先编译(Ahead - of - Time,简称AOT)的编译器和一个支持虚拟机的运行时库。

AOT编译器

AOT编译器将.NET输出的中间语言(IL)代码转换为C++代码。IL2CPP AOT编译器的实际执行文件是il2cpp.exe。在Windows平台,它位于Unity安装路径的Editor\Data\il2cpp目录下;在OSX平台,则位于Contents/Frameworks/il2cpp/build目录内。il2cpp.exe是一个托管代码可执行文件,完全由C#编写。在开发IL2CPP时,我们同时使用.NET和Mono编译器对其进行编译。

il2cpp接收来自Unity自带或Mono编译器生成的托管程序集,并将这些程序集转换为C++代码。这些转换后的C++代码最终由目标部署平台上的C++编译器进行编译。

你可以参考下图理解IL2CPP工具链的作用: IL2CPP工具链

运行时库

IL2CPP的另一部分是支持虚拟机的运行时库。我们主要使用C++代码实现整个运行时库(实际上,其中有一些与平台相关的代码使用了程序集,这属于内部细节)。我们将运行时库称为libli2cpp,它作为静态库被链接到最终的游戏可执行文件中。这样做的主要好处是使整个IL2CPP技术简单且可移植。

你可以通过查看随Unity一起发布的libil2cpp头文件来了解其代码组织方式。在Windows平台,头文件位于Editor\Data\PlaybackEngines\webglsupport\BuildTools\Libraries\libil2cpp\include目录;在OSX平台,头文件位于Contents/Frameworks/il2cpp/libil2cpp目录。例如,il2cpp生成的C++代码与libil2cpp之间的接口API,位于codegen/il2cpp - codegen.h文件中。

运行时的另一个重要部分是垃圾收集器。在Unity 5中,我们使用libgc垃圾收集器,它是典型的贝姆垃圾收集器(Boehm - Demers - Weiser garbage collector)(相对使用保守垃圾回收策略)。不过,我们的libil2cpp设计为可方便集成其他垃圾回收器,目前我们也在研究集成微软开源的垃圾回收器(Microsoft GC)。关于垃圾回收器,我们会在后续文章中专门讨论。

il2cpp是如何执行的?

下面通过一个简单的例子进行说明。我们使用Unity 5.0.1版本,在Windows环境中创建一个全新的空项目。然后创建一个带有MonoBehaviour的脚本文件,并将其作为组件添加到Main Camera上。代码很简单,用于输出“Hello World”:

using UnityEngine;

public class HelloWorld : MonoBehaviour
{
void Start()
{
Debug.Log("Hello, IL2CPP!");
}
}

当我们切换到WebGL平台进行项目生成时,可以使用Process Explorer观察il2cpp的命令行,得到以下内容:

"C:\Program Files\Unity\Editor\Data\MonoBleedingEdge\bin\mono.exe" "C:\Program Files\Unity\Editor\Data\il2cpp/il2cpp.exe" --copy - level=None --enable - generic - sharing --enable - unity - event - support --output - format=Compact --extra - types.file="C:\Program Files\Unity\Editor\Data\il2cpp\il2cpp_default_extra_types.txt" "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\Assembly - CSharp.dll" "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\UnityEngine.UI.dll" "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\il2cppOutput"

这个命令行很长,我们将其拆分来看。Unity首先运行的可执行文件是:

"C:\Program Files\Unity\Editor\Data\MonoBleedingEdge\bin\mono.exe"

下一个参数是il2cpp.exe工具本身:

"C:\Program Files\Unity\Editor\Data\il2cpp/il2cpp.exe"

需要注意的是,剩下的参数是传递给il2cpp.exe,而非mono.exe。在上述例子中,传递给il2cpp.exe的参数有5个:

  • --copy - level=None:指明il2cpp.exe不对生成的C++文件进行复制操作。
  • --enable - generic - sharing:告诉IL2CPP在可行的情况下,对通用方法进行共享,以减少代码量并降低最终二进制文件的大小。
  • --enable - unity - event - support:确保与Unity events相关的、通过反射机制运行的代码能够正确生成。
  • --output - format=Compact:在生成C++代码时,为类型和方法使用更短的名称。这会使C++代码难以阅读,因为IL中的原始名称被更短的名称替代,但可以加快C++编译器的运行速度。
  • --extra - types.file="C:\Program Files\Unity\Editor\Data\il2cpp\il2cpp_default_extra_types.txt":使用默认的(也是空的)额外类型文件。il2cpp.exe会将该文件中出现的基本类型或数组类型视为运行时生成的,而非一开始就存在于IL代码中。

需要注意的是,这些参数可能会在未来的Unity版本中发生变化,目前我们尚未将il2cpp.exe的命令行参数固定下来。

最后,命令行中包含一个由两个文件组成的列表和一个目录:

"C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\Assembly - CSharp.dll"
"C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\UnityEngine.UI.dll"
"C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\il2cppOutput"

il2cpp.exe可以接收一个IL程序集列表。在这个例子中,程序集包括项目中的简单脚本程序集Assembly - CSharp.dll和GUI程序集UnityEngine.UI.dll。你可能会注意到,UnityEngine.dll和系统底层的mscorlib.dll并未列出。实际上,il2cpp.exe会在内部自动引用这些程序集。你可以将它们添加到列表中,但并非必需。你只需提及根程序集(即未被其他任何程序集引用的程序集),il2cpp.exe会根据引用关系自动添加其他程序集。

命令行的最后一部分是一个目录,il2cpp.exe会将最终的C++代码生成到该目录中。如果你对此感兴趣,可以查看该目录中生成的文件,这将是我们下一个讨论的主题。在查看这些代码之前,你可以勾选WebGL构建设置中的“Development Player”选项,这样可以移除--output - format=Compact命令行参数,使C++代码中的类型和方法名称更易读。

尝试在WebGL或iOS构建设置中进行更改,你会发现传递给il2cpp.exe的参数也会相应变化。例如,将“Enable Exceptions”设置为“Full”会将--emit - null - checks--enable - stacktrace--enable - array - bounds - check这三个参数添加到il2cpp.exe命令行中。

IL2CPP没做的事情

需要指出的是,我们没有尝试重写整个C#标准库。当使用IL2CPP后端构建Unity项目时,mscorlib.dllSystem.dll等中的C#标准库与使用Mono编译时完全相同。

我们可以依赖健壮且经过充分测试的C#标准库,因此在处理IL2CPP相关的bug时,我们可以确定问题出在AOT编译器或运行时库,而非其他地方。

我们如何开发、测试、发布IL2CPP

自今年1月的4.6.1 p5版本首次引入IL2CPP以来,我们已经连续发布了6个Unity版本和7个补丁(Unity版本号跨越4.6和5.0),在这些发布中修复了超过100个bug。

为了持续改进IL2CPP,我们内部仅在主干分支(trunk branch)上保留一份最新的开发代码。在发布各个版本之前,我们会将IL2CPP的改动合并到特定分支,然后进行测试,确保所有bug都已正确修复。我们的QA和维护团队为此付出了巨大努力,以保证发布版本的快速迭代。

用户社区提供高质量的bug反馈对我们来说非常宝贵。我们非常感谢用户的反馈,并希望能收到更多此类反馈,以帮助我们进一步改进IL2CPP。

我们的IL2CPP研发团队具有强烈的“测试优先”意识,经常采用“Test Driven Design”方法。在未进行充分全面的测试之前,几乎不会进行代码合并。这种策略在IL2CPP项目中效果显著。目前我们遇到的大部分bug并非由意外行为导致,而是由意外的特殊情况引起(例如在32位索引数组中使用64位指针,导致C++编译器失败)。对于这类bug,我们能够快速且自信地进行修复。

在社区的帮助下,我们致力于让IL2CPP既快速又稳定。顺便一提,如果你对上述内容感兴趣,我们正在招聘相关人员。

后续精彩内容

关于IL2CPP,我们还有很多内容可以分享。下次我们将深入探讨il2cpp.exe代码生成的细节,看看对于C++编译器而言,il2cpp.exe生成的代码是什么样的。

请关注泰斗社区。