lua是脚本语言里面比较流行的一种,因其虚拟机小巧、API丰富、可灵活定制而深受游戏引擎开发商的喜爱。Unity使用了C#和Unity Script(现已废弃)来作为脚本语言。C#语言因为建立在.NET IL之上而具有跨平台扩展性。这样,游戏开发者只需要一套代码就可在多个平台运行。
[ 图六:.NET CIL和CLR ]
2.2 IL是什么?IL(Intermediate Language,在.NET平台下是CIL,Common Intermediate Language)是一种中间语言格式,类似于Java的字节码(byte code),这种格式的代码需要一个虚拟机来“解释”执行。IL的所有指令都是基于虚拟堆栈的:调用函数前,先将参数push到虚拟堆栈里面;函数执行的时候,从虚拟堆栈里面取出参数,然后将结果压入虚拟堆栈。由于调用方式简单,IL语言的指令集也比较精简。
IL作为脚本语言的独到之处在于可以将C#上层语言的各种特性(如泛型、协程等)转换成基本的IL指令集,但是这样的转换也是有代价的 — 转换后的IL指令比普通的函数调用多出数倍。因此,在游戏开发中,不宜在每一帧中都进行这一类的调用。
另外,IL语言执行需要一个虚拟机翻译成目标平台的机器码,虽然.NET虚拟机已经比较高效了(可参考.NET与Java的对比),但是和平台原生代码比起来,依然有一些差距。在iOS平台上,由于苹果禁止使用JIT方式,IL指令需要预先编译成目标平台库文件,然后在最终二进制文件打包的时候作为第三方库链接进去。Unity游戏几乎所有的游戏逻辑都是通过脚本来实现的,一个大型游戏,成千上万个脚本,AOT方式打包造成的效率低下,是不得不考虑的问题。因此,Unity在5.3.4版本中引入了il2cpp技术。
2.3 il2cpp原理顾名思义,il2cpp就是把中间语言转换成cpp代码的工具。上面我们讲到,在iOS平台上,由于无法使用JIT方式执行IL指令,所以需要先将游戏脚本打包成.NET Managed Assembly(这里的Managed是指二进制文件是在.NET层面打包的,可能会依赖.NET底层库,可以理解为“安全的”库文件。另外有些库文件是通过直接封装C/C++接口方式生成的,由于有如指针之类的底层内存操作,所以称作是Unmanaged Assembly),然后和.NET CLR的Assembly链接之后生成最终的平台二进制文件。il2cpp的作用是去掉链接.NET CLR的步骤,将C#脚本生成的Managed Assembly“翻译”成C++文件,最后用目标平台的编译器编译这些C++文件来生成最终的游戏可执行文件。
[ 图七:il2cpp工作原理示意图 ]
il2cpp会先读取.NET二进制文件,解析其中的符号,然后将其中C#方法转换成对应的C方法。虽然名为il2cpp,但其实它只用到了很少部分的C++特性,绝大多数转换后的代码都是C函数。
[ 图八:il2cpp转换后的代码示例 ]
在游戏运行前,il2cpp会启动一个小的虚拟机,用于动态解析C方法。其会将所有方法的签名放在一个叫做global-metadata.dat的文件里,方法调用的时候会先从此文件里读取C函数地址,然后再调用。
获取函数指针的方法是这个:
inline Il2CppMethodPointer il2cpp_codegen_resolve_icall (const char* name){ Il2CppMethodPointer method = il2cpp::vm::InternalCalls::Resolve (name); if (!method) { il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetMissingMethodException(name)); } return method; }