Unity 是一个基于组件式编程的游戏开发引擎,其为开发者提供了很多功能强大的组件。尽管在通过脚本操作Unity 组件的过程中,Unity 有自己的实现和技术,但归根结底,Unity 还是利用了 Mono 运行时来实现其脚本模块的基础。

Mono

Mono 是一个由 Xamarin 公司所主持的自由开放源代码项目。它基于通用语言架构(Common Language infrastructure ,缩写 CLI)和 C#的 ECMA标准(Ecma-334和Ecma-335),提供了微软 .NET 的另一种实现方式。与微软的 .NET Framework(共通语言运行平台)不同,Mono 项目不仅可以运行于 Windows 系统上,还可以运行于Linux,FreeBSD,Unix,OS X 和 Solaris,甚至一些游戏平台,例如:Playstation 3,Wii 或 XBox 360。看到这里,大伙应该知道为什么 Unity 能够实现跨平台了,没错,这归功于 Mono。不过,在 Unity2018版本后,Unity 不再使用 Mono,而采用 .NET Core 来实现跨平台。

.Net是微软的一种技术平台/一种规范,而不是一种语言,可以理解为接口
.NET平台支持多种语言开发:C#、F#、Visual Basic等
目前.Net有三种主流实现

  1. .Net Framework:主要是基于Windows上开发
  2. .Net Core:支持跨平台开发
  3. Mono:支持跨平台开发

Mono与 .Net FrameWork的关系(About Mono | Mono)

.NET和Mono两者是完全独立的,简单的理解:Mono项目是第三方的 .Net Framework 实现,比喻为 .Net 的跨平台版,绝大多数C#的特性在.net和mono中都是相同的,少部分特性两者的实现不一致。

Mono的目标是创建一系列符合ECMA 标准(Ecma-334 和Ecma-335)的 .NET 工具,包括C# 编译器和共通语言执行平台。

Mono 虚拟机包含一个实时编译引擎,该引擎可用于如下处理器:x86,SPARC,PowerPC,ARM,S390(32位模式和64位模式),x86-64,IA64 和64位模式的 SPARC。该虚拟机可以将代码实时编译或者预先编译到原生代码。对于那些没有列出来的系统,则使用的是代码解释器。

Mono 的组成

  1. C#编译器:C# 编译器称为 mcs,可以完成C#的编译工作,作用就是将 C# 源码编译成中间语言CIL,在非Windows平台上需要 Mono 运行时来运行,而在 Windows 平台上既可以用 .Net 运行时,也可以使用Mono运行时。

  2. Mono运行时(Mono VM (Virtual Machine) ): 实现了 ECMA 公共语言架构,提供了一个即时编译器(JIT)、预编译器(AOT)、类库加载器、垃圾回收器、线程系统和互操作性功能。

  3. 基础类库(.Net类库):提供一组全面的类,这些类兼容.Net框架并保持一致,是构建程序的结实基础。

  4. Mono类库:提供了很多超越基础类库的类,提供了额外的功能,例如一些处理 Gtk+,Zip文件,LDAP,OpenGL、Cairo、POSIX 的类等等。

C# 编译器

C# 编译器。 目前最新的 Mono 版本为5.10.1,新版本的 Mono 的 C# 编译器完全兼容 C#1.0至5.0,但由于历史原因,目前所发布的 Unity 的正式版本中,采用的 Mono 版本仍然为2.x 的版本,C#4.0 以后的一些新功能,在 Unity 中得不到支持。Mono 2.11 版本以前,采用过多个编译器,从 2.11 版本开始,采用了一个统一的编译器—— mcs 。由于 mcs 是 C# 编写的,而且使用了很多 .NET 的 API,在非 WIndow 平台上需要使用 Mono 运行时来运行 mcs。但在 Windows 平台上,既可以使用微软的.NET运行时,也可以使用 Mono 运行时。mcs 的作用是将 C#编译成为 ECMA CIL 的中间语言。

Mono 运行时

C# 编译器 mcs 的作用是将 C# 源码编译成中间语言 CIL,Mono 运行时的作用是将 CIL 转换成机器语言。

Mono 运行时提供了三种转译方式:

  1. 即时编译(JIT):在运行过程中,将 CIL 编译成机器码。它是在程序运行时才编译代码,解释一条语句执行一条语句,同时也会将编译过的代码进行缓存,而不是每一次都进行编译。

  2. 提前编译(AOT):在运行前,将 CIL 编译成机器码并储存起来,但还是有一部分编译需要用到 JIT。

注意,JIT,AOT 编译并不是互斥关系,他们可以共存。Mono 的 AOT 模式仍然会保留部分代码在程序运行时采用jit 编译,因为 ios 被禁止了 jit,为了解决这个问题,Mono 提供了一个被称为 Full-AOT 的模式。即预先对程序集中所有 IL 代码进行 AOT 编译生成一个本地代码映像,然后在运行时直接加载这个映像而不再使用jit。

  1. 完全静态编译(Full AOT):在运行前,将所有 CIL 编译成机器码,例如在 IOS 平台上是禁止 JIT 的,所以Mono 只能以 Full AOT 模式运行。

由于 IL 编译为机器码的过程可以以 JIT 的模式进行,因此 C# 也并不能说是一种编译型语言。C# 是一种中间语言,它需要在运行时才能被编译成机器码。这也是为什么 C# 有时候被称为半编译型语言。

image.png

Mono 运行时除提供以上编译器和转译模式外,还提供一个非常重要的功能——垃圾回收器(Garbage Collection,缩写 GC)。GC 的使用,使得开发人员无需去手动管理内存,专注于业务逻辑的开发,而使得开发难度降低,提高开发效率。

然而使用 GC 在获得便利的同时,也会带来一些性能上的损耗。毕竟 GC 在运行中对目标对象的追踪,以及决定是否回收对象内存时,是需要消耗计算机资源的。在 Unity 开发过程中,对于如何避免频繁触发 GC 一直是游戏优化过程中一个老生常谈的问题。Mono 的垃圾回收机制与微软 .NET 框架的不同,在 Mono2.8 版本前后,它有两套不同的垃圾回收机制——分代回收(Generational Collector)和贝姆垃圾收集器(Boehm -Demers-Weiser Garbage Collector)。

Mono 早期版本所采用贝姆垃圾收集器,与微软 .NET 框架的垃圾回收器相比一直有很大的限制,而 Unity 是采用的开源的 Mono,是比较早期的 Mono 版本。因此 Unity 的垃圾回收性能比不上 .NET 的垃圾回收性能,这估计也是 Unity 官方在 2018 版本放弃 Mono 而转投 .NET Core 的一个重要原因吧。

image.png

.Net代码在Mono下的编译过程

托管代码与非托管代码:

  1. 托管代码: 例如C#,Java等,托管代码包含中间语言,需要经过虚拟机/CLR转换为CPU指令,代码执行效率低,但是它不依赖于操作系统和CPU,在各个操作系统上都能执行,它是运行在虚拟机/CLR上的。
  2. 非托管代码:例如C++等,非托管代码是直接对接CPU指令,代码执行效率高,但是不同的操作系统需要单独编写代码,C++ 能跨平台可以理解为每个平台都实现了对应的 C++ 编译器。

首先,我们写的代码通过特定语言的编译器编译成 CIL (Common Intermediate Language:中间语言,字节码,也可以称为IL或CIL),它是一种托管代码,类似于Java的虚拟机,它会存储在.DLL或.EXE的程序集中。CIL是一种伪代码,不能被计算机直接识别。

它与平台操作系统无关也与与CPU无关,是一种中间语言。这也为跨平台奠定了基础。之后在程序运行时再通过 CLR (Common Language Runtime:公共语言运行时) 内部的 JIT 编译器将 CIL 编译成计算机可以识别的 CPU 指令(机器码:01010101)。

IL语言在CLR中运行,IL语言对于CLR来说是透明的,CLR并不知道IL是由哪种语言编译而来,这是一个二次编译的过程。

image.png

IL2CPP

由于 Mono 的一些不足而引发出了一种新的解决方案:IL2CPP,在得到中间语言 IL 后,使用 IL2CPP 将他们重新变回 C++ 代码,然后再由各个平台的 C++ 编译器直接编译成能执行的机器码。

IL2CPP很好理解她的含义:将IL代码转换成CPP文件。现在的大趋势都是把语言加上动态特性,哪怕是 C++ 这样的静态语言,也出现了适合 IL 的 C++ 编译器,那为什么 Unity 要把 IL 再转回静态的 CPP 呢?原因如下:

  1. Mono VM在各个平台移植,维护非常耗时,有时甚至不可能完成。

    Mono 的跨平台是通过 Mono VM 实现的,有几个平台,就要实现几个 VM,像 Unity 这样支持多平台的引擎,Mono官方的 VM 肯定是不能满足需求的。所以针对不同的新平台,Unity 的项目组就要把VM给移植一遍,同时解决 VM 里面发现的 bug,这非常耗时耗力。而且有些平台无法进行移植。

  2. Mono版本授权受限

    因为Mono的授权受限,Unity无法升级 Mono 版本导致一些新的 C# 特性无法使用,如果换作是 IL2CPP,IL2CPP VM这套完全自己开发的组件,就解决了这个问题。

  3. 提高运行效率

    根据官方的实验数据,换成IL2CPP以后,程序的运行效率有了1.5-2.0倍的提升。

image.png