Unity的IL2CPP

目录

背景

概念

AOT编译器

运行时库

对比

Mono

IL2CPP

IL2CPP打包注意事项

类型裁剪

泛型实例

泛型方法


背景

在Unity4.6.1 p5以后版本中,在PlayerSettings—>Other Settings—>Scripting Backend有mono和il2cpp两个选项,它们是Unity脚本后处理(Scripting Backend)的两种方式。

概念

IL2CPP 是 Unity一种新的脚本后处理(Scripting Backend)方式,针对.Net平台编译输出的IL(中间语言-Intermediate Language)进行处理。

IL2CPP主要由两部分组成:

1,AOT静态编译编译器(il2cpp.exe)

预先编译(译注:ahead-of-time,又叫AOT)

2,运行时库(libil2cpp)

其中AOT将IL转换为C++源码,再交给各平台的C++编译器进行编译,达到平台兼容的目的;运行时库则会提供诸如垃圾回收、线程/文件获取、内部调用直接修改托管数据结构的原生代的服务与抽象。

https://images2.imgbox.com/a0/dd/6YBGuMFu_o.png

AOT编译器

IL2CPP AOT编译器实际的执行文件是il2cpp.exe。在Windows平台你可以在Unity安装路径的EditorDatail2cpp目录下找到。对于OSX平台,它位于Unity安装路径的Contents/Frameworks/il2cpp/build目录内。 il2cpp.exe这个工具是一个托管代码可执行文件,其完全由C#写成。在开发IL2CPP的过程中,我们同时使用.NET和Mono编译器对其进行编译。

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

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

https://images2.imgbox.com/14/51/WF7jH6ln_o.png 

运行时库

IL2CPP的另外一个部分就是对虚拟机提供支持的运行时库。我们基本上是用C++代码来实现整个运行时库的(好吧,其实里面还是有一些和平台相关的代码使用了程序集,这个只要你知我知便好,不要告诉别人 )。我们把运行时库称之为libli2cpp,它是作为一个静态库被连接到最终的游戏可执行文件中。这么做的一个主要的好处是可以使得整个IL2CPP技术是简单并且是可移植的。

你能通过查看随Unity一起发布的libil2cpp头文件来窥探其代码组织方式(Windows平台,头文件在EditorDataPlaybackEngineswebglsupportBuildToolsLibrarieslibil2cppinclude目录中。OSX平台,头文件在Contents/Frameworks/il2cpp/libil2cpp目录中)。举个例子,由il2cpp产生的C++代码和libil2cpp之间的接口API,存在于codegen/il2cpp-codegen.h这个文件中。

运行时的另外一个重要的部分,就是垃圾收集器。在Unity 5中,我们使用libgc垃圾收集器。它是一个典型的贝姆垃圾收集器(Boehm-Demers-Weiser garbage collector)。(译注:相对使用保守垃圾回收策略)。然而我们的libil2cpp被设计成可以方便使用其他垃圾回收器。

 

对比

Mono

  • 构建应用非常快。
  • 由于Mono的JIT(Just In Time compilation)机制,所以支持更多托管类库。
  • 支持运行时代码执行。
  • 必须将代码发布成托管程序集(.dll文件,由mono或者.net生成)。
  • Mono VM在各个平台移植异常麻烦,有几个平台就得移植几个VM(WebGL和UWP这两个平台只支持 IL2CPP)。
  • Mono版本授权受限,C#很多新特性无法使用。
  • iOS仍然支持Mono,但是不再允许Mono(32位)应用提交到Apple Store。

https://images2.imgbox.com/28/f7/mFuaf24m_o.png

IL2CPP

相比Mono,代码生成有很大的提高。

  • 可以调试生成的C ++代码。
  • 可以启用引擎代码剥离(Engine code stripping)来减少代码的大小。
  • 程序的运行效率比Mono高,运行速度快。
  • 多平台移植非常方便。
  • 相比Mono构建应用慢。
  • 只支持AOT(Ahead of Time)编译。

https://images2.imgbox.com/03/42/63OkxLZU_o.png

IL2CPP打包注意事项

由于IL2CPP的运行效率的大幅度优势,我们在实际项目中几乎都是直接使用IL2CPP的,在打包过程中可能会遇到一些问题。

IL2CPP和mono的最大区别就是不能在运行时动态生成代码和类型,所以这就要求必须在编译时就完全确定需要用到的类型。

类型裁剪

IL2CPP在打包时会自动对Unity工程的DLL进行裁剪,将代码中没有引用到的类型裁剪掉,以达到减小发布后ipa包的尺寸的目的。然而在实际使用过程中,很多类型有可能会被意外剪裁掉,造成运行时抛出找不到某个类型的异常。特别是通过反射等方式在编译时无法得知的函数调用,在运行时都很有可能遇到问题。

Unity提供了一个方式来告诉Unity引擎,哪些类型是不能够被剪裁掉的。具体做法就是在Unity工程的Assets目录中建立一个叫link.xml的XML文件,然后按照下面的格式指定你需要保留的类型:

<linker>

  <assembly fullname="UnityEngine" preserve="all"/>

  <assembly fullname="Assembly-CSharp">

    <namespace fullname="MyGame.Utils" preserve="all"/>

    <type fullname="MyGame.SomeClass" preserve="all"/>

  </assembly> 

</linker>

泛型实例

每个泛型实例实际上都是一个独立的类型,List<A>和 List<B>是两个完全没有关系的类型,这意味着,如果在运行时无法通过JIT来创建新类型的话,代码中没有直接使用过的泛型实例都会在运行时出现问题。

这个问题有两种方式,一个是使用CLR绑定,把用到的泛型实例都进行CLR绑定。另外一个方式是在Unity主工程中,建立一个类,然后在里面定义用到的那些泛型实例的public变量。这两种方式都可以告诉IL2CPP保留这个类型的代码供运行中使用。

泛型方法

跟泛型实例一样,foo.Bar<TypeA>和foo.Bar<TypeB>是两个完全不同的方法,需要在主工程中显式调用过,IL2CPP才能够完整保留,因此需要尽量避免在热更DLL中调用Unity主工程的泛型方法。如果在iOS上实际运行遇到报错,可以尝试在Unity的主工程中随便写一个static的方法,然后对这个泛型方法调用一下即可,这个方法无需被调用,只是用来告诉IL2CPP我们需要这个方法。

参考资料:

Unity之IL2CPP - 知乎

用Unity做游戏,你需要深入了解一下IL2CPP

【Unity3D字节跳动面试题】Unity底层如何处理C#_哔哩哔哩_bilibili