Mono运行时

前言

Mono 是 Unity 引擎早期使用的托管运行时环境,也是 .NET 生态在跨平台领域最重要的开源实现之一。自 Unity 1.0 起,Mono 就作为默认的脚本后端为游戏逻辑提供执行环境——编辑器模式下 Unity 至今仍然使用 Mono 运行时,确保快速的代码迭代和调试体验。

理解 Mono 运行时,对于掌握 HybridCLR 的技术定位至关重要。这是因为:

第一,Mono 是最早实现 "AOT + JIT + Interpreter" 混合执行模式的 .NET 运行时之一。HybridCLR 的设计哲学——在纯 AOT 平台通过 Interpreter 实现动态代码执行——与 Mono 的混合执行模式有着深刻的渊源。事实上,HybridCLR 官方文档中明确提到其核心思想"来自 Mono 的混合执行模式(Mixed Mode Execution)技术"。

第二,Mono 的架构是理解整个 .NET 运行时体系的绝佳入口。Mono 虽然不如 .NET Core(现 .NET 6/7/8)功能全面,但它的模块设计更加清晰简洁——运行时核心、GC、JIT/AOT 编译器、元数据加载器各司其职,非常适合作为学习 .NET 运行时原理的起点。

第三,HybridCLR 不依赖 Mono——这可能是许多开发者最容易误解的一点。HybridCLR 的设计目标是在 IL2CPP 的纯 AOT 运行时中嵌入解释器模块,而非在 Mono 运行时之上叠加一层。理解 Mono 的架构与 HybridCLR 的架构之间的本质差异,有助于开发者避免在方案选型和问题排查时产生误判。

本文将从 Mono 的整体架构入手,深入剖析其 IL 执行模型、GC 机制、AOT 模式,然后重点阐述 Mono 与 HybridCLR 的真实关系,最后介绍 Mono 在 .NET 统一时代的最新发展。


一、Mono 运行时的架构

1.1 Mono 的整体架构

Mono 是一个遵循 ECMA-335 标准的 .NET 运行时实现,其核心架构可以分为三个主要层次。

运行时核心层(Execution Engine)。这是 Mono 的心脏,负责管理代码执行的完整生命周期。它包括类型系统(MonoClass、MonoMethod 等核心数据结构)、元数据加载器(从 PE 文件解析 IL 和元数据表)、方法调用分发机制,以及线程管理、同步原语(monitor/lock)、AppDomain 管理等基础设施。运行时核心层向下与操作系统交互(内存分配、线程调度),向上为托管代码提供执行环境。在 Mono 中,所有的类型信息在加载时被解析为运行时内部的数据结构,这些结构串联起了 .NET 类型系统的全部信息——继承层次、接口实现、虚函数表布局、字段偏移等。

GC 层(Garbage Collector)。Mono 早期版本使用自己的 GC 实现(Mono GC),它是一款分代式(Generational)标记-清扫(Mark-Sweep)GC。Mono 2.8 之后引入了可插拔的 GC 架构,默认 GC 切换为 SGen(Simple Generational GC),同时也支持使用 Boehm-Demers-Weiser 保守式 GC 作为备选。Unity 对 Mono 进行了定制化改造,在早期版本中使用 Boehm GC,之后切换为 SGen GC。GC 层负责管理托管堆对象的内存分配和回收,其效率直接影响游戏的帧率稳定性和内存占用。

编译后端层(JIT/AOT Compiler)。这是 Mono 实现 IL 代码执行的引擎。Mono 包含一个完整的 JIT 编译器,它在运行时将 IL 字节码编译为目标平台的机器码。Mono 的 JIT 编译器经过多次迭代优化,从最初的简单翻译发展到支持寄存器分配(使用线性扫描算法)、内联展开、分支优化等标准编译优化技术。除了 JIT 模式,Mono 还支持 AOT 编译模式——在构建时将 IL 预编译为原生代码,生成共享库(.so/.dll/.dylib)供运行时加载。此外,Mono 5.0 之后引入了 Interpreter(解释器)模式,可以在纯解释执行和 JIT 编译之间动态切换。

┌──────────────────────────────────────────────────────────────────┐
│                        Mono 运行时架构                            │
├──────────────┬───────────────────────┬───────────────────────────┤
│  运行时核心层  │       GC 层           │      编译后端层             │
│              │                       │                           │
│  类型系统      │  Boehm GC(保守式)    │  JIT 编译器                │
│  元数据加载器  │  SGen GC(分代式)     │  AOT 编译器(Full/Partial) │
│  方法分发      │  可插拔 GC 接口        │  Interpreter(Mono 5.0+)  │
│  线程/同步     │                       │                           │
│  AppDomain    │                       │                           │
├──────────────┴───────────────────────┴───────────────────────────┤
│                        平台抽象层                                  │
│    内存管理(MonoMemoryManager)│ 线程 API │ 信号量 │ 文件 I/O       │
└──────────────────────────────────────────────────────────────────┘

1.2 Mono 与 .NET 的兼容性层次

Mono 对 .NET 的兼容性可以分为三个层次,理解这些层次有助于评估 Mono 在不同场景下的适用性。

API 兼容性。Mono 实现了 .NET Framework 的绝大部分核心 API,包括 mscorlib、System、System.Core、System.Xml、System.Numerics 等基础程序集中定义的数千个公共类型和方法。对于 Unity 开发者来说,日常开发中使用的大部分 .NET API(如 List<T>Dictionary<K,V>LINQasync/awaitTaskFileStream 等)在 Mono 下都能正常工作。但在某些边界 API 上存在差异——特别是与 Windows 平台紧密相关的 API(如 Registry、WMI、某些 System.Management 功能),以及部分较新的 .NET API(如 Span、Memory 在早期 Mono 版本中缺失)。

CLI 规范兼容性。Mono 遵循 ECMA-335 标准实现 CLI(Common Language Infrastructure)。在类型系统层面,Mono 完整支持 CTS(Common Type System,通用类型系统)定义的值类型和引用类型体系,支持接口多继承、泛型(包括协变和逆变)、委托、枚举等所有核心类型特性。在元数据层面,Mono 完整解析 ECMA-335 定义的元数据流格式(~表流、#String 流、#Blob 流等),支持自定义特性(Custom Attribute)的完整反射。在执行模型层面,Mono 的 JIT 编译器可以执行所有标准 IL 指令(约 250 条),包括 ldtoken、constrained、readonly 等高级指令。

运行时行为兼容性。这是兼容性最深层的维度。Mono 在 GC 行为(分代策略、触发阈值)、JIT 编译策略(内联启发式、泛型共享策略)、异常处理(栈展开、过滤器)等方面与官方的 .NET Framework / .NET Core 运行时存在差异。这些差异通常不会影响普通代码的正确性,但在极端场景(如高性能计时器、大量小对象分配、深层递归调用)中可能表现为行为不一致。例如,Mono 的 SGen GC 是分代 GC 但并非像 .NET Core 的 GC 那样拥有 3 代 + 大对象堆的精细分层,因此在某些分配模式下的性能特征可能不同。

1.3 Mono 在 Unity 中的应用历史

Mono 在 Unity 中的演进历程,实际上反映了 Unity 技术在托管运行时选型上的战略变迁。

Mono 主导时期(Unity 1.0 - 2017)。从 Unity 1.0(2005 年)开始,Mono 就是 Unity 唯一的脚本后端。开发者编写的 C# 代码在 Editor 和运行时都通过 Mono 执行。在桌面平台和 Android 上,Mono 使用 JIT 模式;在 iOS 上,由于苹果禁止 JIT,Unity 使用 Mono 的 AOT 模式(Full AOT)将 IL 预编译为原生代码。这一时期,Mono 的发展与 Unity 深度绑定——Unity 维护着自己的 Mono 分支(基于 Mono 的某个稳定版本进行定制化修改),添加了 Unity 需要的特殊功能(如 iOS AOT 优化、特定平台的内存管理适配)。

IL2CPP 崛起时期(2015 - 2019)。Unity 2015 年左右开始研发 IL2CPP,目标是在保持 Mono 的 C# 开发体验的同时,获得更好的性能、更小的包体积和更广泛的原生平台兼容性。IL2CPP 在 Unity 5.0 中首次亮相,起初只支持 iOS 平台,随后逐步扩展到 Android、Windows、macOS 等所有目标平台。到 Unity 2019 LTS 版本,IL2CPP 已经成为 iOS 等 AOT 平台的默认编译后端。但即使在 IL2CPP 成为主流后,Unity Editor 模式下的脚本执行仍然依赖 Mono——因为 Mono 的 JIT 模式提供了更快的代码迭代速度和更完整的调试体验。

混合运行时时期(2019 - 至今)。从 Unity 2019 开始,Unity 进入了一个"双运行时"并存的时代:Editor 模式下使用 Mono(JIT),发布模式下默认使用 IL2CPP(AOT)。开发者可以在 Player Settings 中自由切换编译后端,但 Unity 官方推荐在生产发布中使用 IL2CPP。HybridCLR 正是在这一背景下诞生的——它通过增强 IL2CPP 来解决 AOT 平台的动态代码执行问题,而不是回到 Mono 的老路上。

时期 时间范围 Editor 运行时 发布运行时 关键事件
Mono 主导期 2005-2015 Mono JIT Mono JIT / Mono AOT Unity 1.0 到 Unity 5.x
IL2CPP 崛起期 2015-2019 Mono JIT IL2CPP(iOS)+ Mono(其他) IL2CPP 首次引入,逐步推广
混合运行时时期 2019-至今 Mono JIT IL2CPP(默认) IL2CPP 成为全平台默认后端
HybridCLR 时期 2022-至今 Mono JIT IL2CPP + HybridCLR Interpreter HybridCLR 为 IL2CPP 注入解释器能力

二、Mono 的 IL 执行模型

2.1 IL 指令集的执行流程

理解 Mono 的 IL 执行模型,需要从 IL(Intermediate Language,中间语言)的本质说起。IL 是一种与 CPU 架构无关的栈式指令集,它描述的是方法级别的计算逻辑而非完整的程序执行流程。当 C# 代码被编译为程序集时,每个方法的实现体被编码为一个 IL 指令流——包含操作码(opcode)和操作数(operand)。Mono 运行时的任务就是将这些 IL 指令转化为实际的 CPU 操作。

整个执行流程从程序集加载开始。当 Mono 运行时加载一个 .NET 程序集(DLL)时,首先解析其 PE(Portable Executable)文件结构,提取 CLI 头(CLR Header)指向的元数据信息。元数据包含类型定义(TypeDef)、方法定义(MethodDef)、字段定义(FieldDef)等表结构,以及 IL 指令流本身——方法定义中的 RVA(Relative Virtual Address)字段指向 IL 指令在文件中的偏移位置。Mono 将这些信息解析为运行时内部的数据结构(MonoImage、MonoClass、MonoMethod),建立起完整的类型系统。

当某个方法被首次调用时,触发 JIT 编译过程(在 JIT 模式下)。Mono 的 JIT 编译器读取该方法的 IL 指令流,经过解码、分析、寄存器分配、机器码生成等阶段,最终生成目标 CPU 架构(ARM、x86、x64 等)的原生机器码。生成的机器码被存储在内存中的代码缓存区,后续对该方法的调用将直接跳转到已编译的机器码执行,不再经过 JIT 编译器。这种"按需编译"的策略确保了运行时只编译那些实际被调用的方法,避免了无谓的编译开销。

2.2 栈式执行引擎

Mono 的 IL 执行基于评估栈(Evaluation Stack)的栈式执行模型。这是 ECMA-335 规范定义的标准执行模式,也是理解 .NET 运行时工作原理的基础。

评估栈是一个逻辑上的栈结构,它独立于操作系统的线程栈。每条 IL 指令从栈上弹出操作数,执行运算,然后将结果压回栈中。例如,执行 int c = a + b 这样的 C# 代码,其对应的 IL 指令序列为:

ldloc.0    // 将局部变量 a(索引0)压栈
ldloc.1    // 将局部变量 b(索引1)压栈
add        // 弹出栈顶两个值,相加,将结果压栈
stloc.2    // 弹出栈顶值,存入局部变量 c(索引2)

在这个执行过程中,评估栈上经历了以下状态变化:

初始: 栈为空
ldloc.0 后: [a]
ldloc.1 后: [a, b]
add 后:    [a + b]
stloc.2 后: [] (值已存入局部变量 c)

Mono 的 JIT 编译器在编译 IL 时,会将这种栈式操作转换为寄存器操作。例如,ldloc.0 可能被编译为从内存(栈帧中的局部变量槽)加载到寄存器,add 被编译为寄存器加法指令,stloc.2 被编译为将结果存回内存。这种从栈式到寄存器的转换是 JIT 编译的核心工作之一,Mono 的 JIT 编译器使用线性扫描算法(Linear Scan Register Allocation)来管理有限的物理寄存器资源,尽量将栈操作转换为寄存器操作以减少内存访问。

评估栈的一个重要特性是它总是与方法的执行帧(Frame)绑定。当运行时进入一个方法时,会为其创建一个执行帧,其中包含方法参数区、局部变量区和评估栈。评估栈的深度在编译时由 IL 分析确定(maxstack 字段),运行时据此分配栈帧空间。这种设计使得评估栈的管理非常高效——本质上是一块预先分配好的内存区域,加上一个栈顶指针(stack pointer)管理压栈和弹栈。

2.3 异常处理机制

Mono 的异常处理机制遵循 ECMA-335 定义的异常模型,基于受保护区域(Protected Region)和方法级别的异常处理表(Exception Handler Table)。

异常处理表的结构。每个方法在元数据中关联着一张异常处理表,其中包含若干异常处理子句(Exception Clause)。每个子句定义了:

  • 受保护区域(Try Block):使用 IL 偏移量的范围(from 到 to)标识
  • 处理区域(Handler Block):对应的 catch/finally/fault 块的范围
  • 处理类型(Handler Type):catch(类型匹配异常)、finally(始终执行)、fault(仅在异常时执行)、filter(条件过滤)
  • 捕获类型(Catch Type):仅 catch 类型需要,指定捕获的异常类型

运行时异常处理流程。当运行时的 IL 执行过程中抛出异常(执行 throw 指令或运行时检测到错误如 NullReferenceException),Mono 的异常处理系统会启动以下流程:

  1. 栈展开(Stack Unwinding):运行时从当前方法开始,沿着调用栈向上遍历每个方法的执行帧。对于每个帧,运行时查询其异常处理表,检查引发异常的 IL 偏移量是否落在某个受保护区域内。

  2. 匹配异常处理子句:如果找到了包含异常位置的受保护区域,运行时依次检查其关联的处理子句。对于 catch 子句,检查抛出的异常类型是否匹配(包含子类型匹配);对于 filter 子句,执行过滤条件代码判断是否匹配;对于 finally 子句,标记为需要执行但暂时不执行(最后执行)。

  3. 执行异常处理代码:找到匹配的 catch 或 filter 子句后,运行时将执行栈展开到该子句对应的帧,跳转到 catch/filter 代码块。如果遍历了所有帧都未找到匹配的处理子句,未处理的异常将送达运行时的顶层异常处理器,通常导致进程终止(在 Unity 中则是报告异常后停止脚本执行)。

  4. finally 块的执行:在栈展开过程中,对于每个离开的帧(无论是正常返回还是异常展开),如果该帧有 finally 块且异常位置位于 finally 块的受保护区域内,该 finally 块会被立即执行。这保证了 finally 块的确定性执行——无论方法是通过正常返回还是异常退出,finally 块都会被执行。

Mono 的实现细节。Mono 的 JIT 编译器在编译方法时,将 IL 的异常处理结构映射为目标平台的底层异常处理机制。在 Linux/Android 上,Mono 使用 setjmp/longjmp 结合信号处理器(signal handler)来捕获异常;在 Windows 上,Mono 使用 SEH(Structured Exception Handling,结构化异常处理,通过 __try/__except/__finally)。JIT 编译后的代码在抛出异常时,通过 Mono 运行时提供的 mono_handle_exception 函数进行统一的栈展开和异常分发。这种设计的优势在于——JIT 编译的机器码不需要包含复杂的异常处理元数据,所有的异常处理逻辑都由运行时核心集中管理。

2.4 JIT 编译和解释执行

Mono 运行时支持多种 IL 代码执行模式,这种灵活性是其跨平台能力的关键。

JIT 模式(默认模式)。这是 Mono 最常用的执行模式。在 JIT 模式下,当方法首次被调用时,Mono 的 JIT 编译器将其 IL 编译为原生机器码,缓存编译结果,后续调用直接使用缓存。Mono 的 JIT 编译器支持多级优化:默认编译(快速编译,轻度优化)和优化编译(使用 -O=all 标志,执行更激进的优化如内联展开、循环不变代码外提等)。在 Unity Editor 中,Mono 以 JIT 模式运行,确保快速的脚本迭代和完整的调试体验。

AOT 模式(预编译模式)。在 iOS 等禁止内存执行代码的平台上,Mono 退化为 AOT 模式。Mono 的 AOT 编译器在构建阶段将 IL 预编译为原生共享库(iOS 上是 .a 静态库,Android 上是 .so 共享库)。运行时加载这些预编译的代码,不再需要 JIT 编译器。Mono 的 AOT 模式有 Full AOT(完全 AOT,所有代码预编译)和 Partial AOT(部分 AOT,只预编译部分代码,其余代码使用 JIT 或解释器)两种变体。

Interpreter 模式(Mono 5.0+)。Mono 5.0 引入了内置解释器(Mono Interpreter),它可以直接逐条解释执行 IL 指令,不需要编译为机器码。解释器模式不是孤立存在的——Mono 实现了 JIT 和 Interpreter 的混合执行模式(Mixed Mode Execution)。在这种模式下,运行时可以根据方法的执行频率在解释器和 JIT 编译器之间切换:冷代码(很少执行的方法)使用解释器,热代码(频繁执行的方法)升级为 JIT 编译。这种混合模式既保留了 JIT 的高性能,又减少了对可写内存执行的需求。

Mono 混合执行模式的切换机制

方法首次调用 → 解释器执行(计数归零)
    │
    ├── 执行计数达到阈值 → JIT 编译器编译 → 机器码执行
    │
    └── 执行计数未达标 → 持续解释执行

这个切换机制的核心是方法调用计数器。每个方法在运行时关联一个计数器,每次调用递增。当计数器达到预设阈值(通常为几十到几百次,取决于运行时配置),该方法被标记为"热方法",触发 JIT 编译。这种自适应策略确保了大部分短暂执行的方法不会触发 JIT 编译(节省编译时间和代码缓存),而频繁执行的热点方法则会获得 JIT 编译的性能优势。

HybridCLR 的 Interpreter 设计理念与 Mono 的混合执行模式有着明显的渊源关系。区别在于:Mono 的 Interpreter 和 JIT 在同一个运行时内切换,而 HybridCLR 的 Interpreter 嵌入在 IL2CPP(一个纯 AOT 运行时)中,执行的是自定义寄存器指令而非原始 IL 指令。


三、Mono 的 GC 机制

3.1 分代 GC 的工作原理

Mono 默认使用 SGen(Simple Generational GC,简洁分代式 GC),这是一款分代式、精确扫描、压缩式的垃圾回收器。SGen 的设计目标是在保持良好性能的前提下提供确定性的 GC 行为,特别适合游戏等对帧率稳定性有要求的应用。

分代策略。SGen 将托管堆划分为两代:

  • 第 0 代(Nursery,新生代):一个连续的、固定大小的内存区域(默认为 4MB,可通过 MONO_GC_PARAMS 环境变量配置)。所有新分配的对象首先在第 0 代分配。第 0 代的 GC 频率最高,因为大多数对象是"朝生夕死"(Young Generation Hypothesis)的——临时对象、方法执行过程中分配的中间对象,通常在方法结束后就不再被引用。第 0 代的 GC 是 SGen 中最快、最频繁的 GC 类型。

  • 第 1 代(Major Heap,老年代):存储从第 0 代回收后存活下来的对象,以及大对象(超过 Large Object Space 阈值的对象,通常为 8KB)。第 1 代的 GC 触发频率远低于第 0 代,但每次 GC 的耗时更长,因为需要扫描更大的堆区域。SGen 对第 1 代执行的是完整标记-清扫-压缩(Mark-Sweep-Compact)算法。

分配与回收流程。SGen 的分配策略非常高效。由于第 0 代是连续内存区域,新对象的分配本质上是一次指针碰撞(bump-pointer allocation)——将分配指针向前移动对象大小即可,不需要搜索空闲列表。这使得小对象的分配速度接近栈分配。

当第 0 代空间耗尽时,触发 Minor GC(小型 GC)。Minor GC 的步骤为:

  1. 标记阶段(Mark Phase):从 GC 根(Roots)出发,遍历所有可达对象,标记第 0 代中存活的对象。GC 根包括:线程栈上的局部变量和参数、静态字段、GC Handle(如 GCHandle)等。SGen 使用精确扫描(Precise Scanning)——通过运行时的元数据信息精确知道每个栈帧中哪些位置是对象引用,哪些是纯数值,避免了保守式 GC 的误判问题。

  2. 清扫阶段(Sweep Phase):遍历第 0 代中的所有对象,将未被标记的对象视为垃圾,回收其占用的内存空间。

  3. 提升阶段(Promotion Phase):将标记为存活的对象从第 0 代移动(evacuate)到第 1 代。这个过程实际上是一次内存复制——因为第 0 代是完整的连续区域,回收后整个区域需要清空供新一轮分配使用。对象被复制到第 1 代后,其在第 0 代的原始位置被标记为无效。

当第 1 代堆空间不足或应用程序主动调用 GC.Collect 时,触发 Major GC(完整 GC)。Major GC 扫描所有代(包括第 0 代和第 1 代),执行完整的标记-清扫-压缩流程。Major GC 的成本远高于 Minor GC,因为它需要扫描整个托管堆。

3.2 写屏障与读屏障

写屏障(Write Barrier)是分代 GC 实现正确性的关键技术。它的必要性源于一个核心问题:Minor GC 只扫描第 0 代的对象,但第 1 代的对象可能持有指向第 0 代对象的引用。如果不处理这些"跨代引用",Minor GC 可能错误地将一个仍然存活的对象(被第 1 代对象引用)判定为垃圾并回收。

写屏障的机制。SGen 在每次对象字段写入操作(如 obj.field = value)后插入一段检查代码——这就是写屏障。写屏障检查写入的值是否指向第 0 代对象,以及目标对象是否位于第 1 代。如果条件满足(即老年代对象被写入了一个指向新生代对象的引用),SGen 将该老年代对象记录到一个特殊的"记忆集"(Remembered Set)中。

记忆集在 Minor GC 中的作用至关重要。当 Minor GC 开始标记时,它不仅从栈和静态区遍历根引用,还从记忆集中遍历所有被记录的老年代对象,确保所有被老年代引用的新生代对象被正确标记。这样,Minor GC 无需扫描整个第 1 代堆,只需要扫描记忆集中被记录的第 1 代对象,大大减少了标记工作量。

写屏障的性能开销通常很小(只有几条 CPU 指令),但它对分代 GC 的整体性能影响是质变的——没有写屏障,分代 GC 就无法高效工作的理论基础。

读屏障的角色。与写屏障不同,读屏障在 .NET 主流的 GC 实现中并不常见。但 Mono 的 SGen 在特定场景下使用了读屏障——当对象从第 0 代被提升到第 1 代时,所有持有该对象引用的位置都需要更新地址(因为对象在内存中被移动了)。SGen 在读屏障中记录对这些对象的访问,确保在对象移动后引用被正确更新。读屏障在 SGen 中仅在压缩(Compact)阶段使用,不是 GC 的核心机制,但在避免 dangling pointer 上发挥了重要作用。

3.3 Mono GC 与 IL2CPP GC 的差异

理解 Mono GC 与 IL2CPP GC 的差异,对于评估两种运行时的性能特征和选择编译后端有着实际意义。

维度 Mono GC(SGen) IL2CPP GC(基于 Unity 的 GC)
GC 类型 分代式(2 代) 分代式(Boehm 或增量式)
扫描方式 精确扫描(Precise) 精确扫描(Unity 5.0+)
压缩 支持(Major GC 时) 支持
写屏障 有(记忆集维护) 有(Card Table)
大对象处理 大对象堆(LOS,≥ 8KB) 大对象堆(≥ 8KB)
GC 触发策略 分配指针 + 代空间阈值 分配指针 + 堆大小阈值
增量 GC 不支持(SGen 版本) Unity 的增量式 GC(增量式标记-清扫)

最关键的区别在于 增量式 GC(Incremental GC)的支持。IL2CPP 运行时中的 Unity GC(基于 Boehm GC 的定制版本)支持增量式 GC——它将一次完整的 GC 暂停分散到多个帧中执行,每帧只执行一小部分 GC 工作,从而减少单帧的 GC 暂停时间。这对于需要保持 30fps 或 60fps 帧率的游戏至关重要。而 Mono 的 SGen GC 在标准的 Unity 集成中不支持增量模式,Major GC 触发时可能导致数百毫秒的帧率卡顿。

HybridCLR 使用的是 IL2CPP 运行时的 GC,而不是 Mono 的 GC。这是 HybridCLR "不依赖 Mono"的一个有力证据——HybridCLR 的热更新代码分配的对象由 IL2CPP 的 GC 管理,遵循 IL2CPP 的 GC 策略(包括增量式 GC),而非 Mono 的 SGen 策略。

3.4 GC 性能特征

Mono GC 的性能特征可以从分配效率、回收效率和暂停时间三个维度来评估。

分配效率。SGen 的小对象分配是典型的高速路径(fast path):检查第 0 代剩余空间是否足够,如果足够,执行指针碰撞分配。这个过程在优化后只需要几条原子指令(atomic add),吞吐量极高。当第 0 代空间不足时,分配走慢速路径(slow path),触发 Minor GC 或分代扩容。在实际的 Unity 游戏场景中,慢速路径的触发频率取决于第 0 代大小和对象的分配速率——第 0 代越大,Minor GC 触发频率越低,但每次 Minor GC 的时间越长。

回收效率。Minor GC 的效率非常高,因为它只处理第 0 代的有限对象。典型情况下,Minor GC 的暂停时间为 1-5 毫秒,足以满足游戏场景的需求。Major GC 的效率则取决于存活对象的总量和堆的碎片化程度。在存活对象比例高的场景中(类似服务器的稳态内存使用),Major GC 的标记阶段需要遍历整个对象图,耗时可能达到 50-200 毫秒。

暂停时间特征。Mono GC(SGen)是典型的 Stop-The-World GC——在 GC 执行期间,所有托管线程都被暂停。这意味着 GC 暂停时间直接表现为游戏的帧率卡顿。对于第 0 代 GC,暂停时间通常可以接受(<5ms)。但对于 Major GC,尤其是在堆较大(数百 MB)且存活对象较多的情况下,暂停时间可能超过 100ms,导致明显的帧率抖动。这也是为什么 Unity 推荐使用 IL2CPP + 增量式 GC 组合的原因——Mono 的 Major GC 暂停在高分辨率纹理和复杂场景下可能成为性能瓶颈。


四、Mono 的 AOT 模式

4.1 Mono AOT 编译的工作原理

Mono 的 AOT 编译是 Mono 在禁止 JIT 平台上(如早期的 iOS)的解决方案。其核心思想是:在应用程序构建阶段,将托管代码的 IL 预编译为原生机器码,生成平台相关的共享库,运行时直接加载预编译的机器码执行,完全不依赖 JIT 编译器。

Mono AOT 的编译流程分为几个阶段:

第一阶段:IL 读取与解码。AOT 编译器读取目标程序集的 PE 结构,解析元数据表和 IL 指令流。这一阶段与 JIT 编译器的前端(Frontend)完全一致——无论是 AOT 还是 JIT,Mono 都使用相同的元数据加载器和 IL 解码器。

第二阶段:IR 生成与优化。Mono 的 AOT 编译器将 IL 指令转换为内部的中间表示(IR,Intermediate Representation)。Mono 的 IR 基于寄存器传输语言(RTL,Register Transfer Language),是一种与目标架构无关的低级表示。在这个表示上,AOT 编译器可以执行多种优化:常量传播(Constant Propagation)、死代码消除(Dead Code Elimination)、循环优化(Loop Optimization)、内联展开(Inlining)等。由于 AOT 编译发生在构建阶段,时间预算更宽松,AOT 编译器可以执行更激进的优化策略。

第三阶段:机器码生成。优化后的 IR 被调度到目标架构的代码生成器(Code Generator)。Mono AOT 支持多种目标架构:ARM、ARM64、x86、x64、MIPS 等。代码生成器负责指令选择(Instruction Selection)、寄存器分配(Register Allocation,使用线性扫描算法)、指令调度(Instruction Scheduling)和最终的机器码编码(Code Emission)。

第四阶段:共享库输出。生成的机器码被写入目标文件(.o 文件),然后通过平台的原生链接器(ld)链接为共享库(.so 文件)或静态库(.a 文件)。链接后的产物包含了所有 AOT 编译的方法的机器码,以及必要的运行时元数据信息。

Mono AOT 的产物加载。在运行时,Mono 的 AOT 加载器首先读取 AOT 编译产物中的元数据信息,将这些预编译的方法注册到运行时的类型系统中。当这些方法被调用时,运行时直接查找到对应的预编译机器码地址并执行,不需要经过 JIT 编译器。如果在运行时尝试调用一个没有经过 AOT 编译的方法(比如通过反射调用的动态生成类型),Mono 的行为取决于配置——如果设置为 Full AOT 模式(如 iOS),运行时将直接抛出 ExecutionEngineException;如果设置为 Hybrid AOT 模式,运行时可以回退到 JIT 编译或解释器执行。

4.2 Mono AOT vs IL2CPP 的差异

Mono AOT 与 IL2CPP 虽然都能将 IL 预编译为原生代码,但它们的技术路线存在根本性差异。

维度 Mono AOT IL2CPP
中间表示 Mono IR(RTL,寄存器传输语言) C++ 源代码
机器码生成 Mono 自身的代码生成器 平台原生编译器(clang/MSVC)
目标架构支持 ARM/ARM64/x86/x64(由 Mono 代码生成器决定) 所有支持 C++ 编译器的平台
代码质量 中等(Mono 代码生成器不如 LLVM 成熟) 高(利用 LLVM/clang 的深度优化)
包体积 较大(包含完整运行时 + 元数据 + AOT 代码) 较小(IL2CPP 可裁剪未使用的代码和元数据)
泛型处理 泛型共享(Generic Sharing) 显式实例化 + 泛型共享
运行时代码执行 不支持(Full AOT) 不支持(纯 AOT)
维护成本 需自行维护所有目标架构的代码生成器 只需维护 C++ 代码生成逻辑

最本质的差异在于代码生成的架构。Mono AOT 使用自己的代码生成器为每个目标架构生成机器码——这意味着 Mono 团队需要维护多套后端(ARM 的指令选择、x86 的寄存器分配、ARM64 的 ABI 适配等),每套后端的质量直接影响代码生成效率和稳定性。IL2CPP 则完全不同——它生成 C++ 代码,将底层的机器码生成工作完全外包给 Clang、MSVC、GCC 等经过数十年打磨的工业级编译器。这种架构选择使得 IL2CPP 可以快速支持新平台(只需要 C++ 编译器支持即可),同时代码生成质量直接受益于 LLVM 等编译基础设施的持续改进。

性能对比。在实际测试中,IL2CPP 生成的代码通常优于 Mono AOT。主要原因是 IL2CPP 生成的 C++ 代码经过 LLVM 的完整优化管线(包括 IPO、内联、向量化、自动并行化等),而 Mono AOT 的代码生成器在优化深度上无法与 LLVM 匹敌。IL2CPP 在某些场景下的性能甚至优于标准的 .NET JIT 编译代码,尽管 JIT 拥有运行时的 profile 信息优势。

包体积对比。IL2CPP 在代码裁剪方面具有显著优势。IL2CPP 的 Managed Code Stripping(托管代码裁剪)机制可以在编译时移除所有未被引用的代码和元数据,最终的产物体积通常比 Mono AOT 小 30%-50%。对于移动游戏来说,减小包体积直接关系到用户的下载转化率。

4.3 Mono AOT 的局限

Mono AOT 虽然有"预编译解决 JIT 禁令"的价值,但它的局限同样突出——这也是 Unity 最终推出 IL2CPP 并替代 Mono AOT 的根本原因。

泛型支持不完整。这是 Mono AOT 最大的痛点。在 JIT 模式下,泛型实例化是"按需编译"的——List<Foo> 在代码中第一次出现时才被编译。但在 AOT 模式下,编译器无法预知所有的泛型实例化组合,特别是当泛型参数是用户定义的类型时。Mono AOT 通过"泛型共享"(Generic Sharing)技术缓解这个问题——将引用类型参数统一为指针大小处理,值类型参数才生成特定代码。但泛型共享并非万能:对于包含值类型参数的泛型方法、对于嵌套的泛型结构、对于泛型虚方法中的某些场景,仍然需要预先声明或遭遇运行时异常。在 Unity 项目中,Mono AOT 的泛型限制经常导致 iOS 构建失败——开发者必须在 Link.xml 中手动列出所有可能用到的泛型实例化,否则运行时可能抛出 MissingMethodException。

反射能力受限。与泛型问题类似,Mono AOT 无法处理运行时通过字符串名称动态查找的类型。Type.GetType("SomeType") 只有在 "SomeType" 在 AOT 编译时已知时才有效。Reflection.Emit 完全不可用,因为 AOT 模式下运行时的代码生成被禁止。这种限制意味着许多依赖反射的高级框架(如部分 IoC 容器、动态代理生成器)在 Mono AOT 下需要特殊的适配处理。

异常处理的性能开销。Mono AOT 在异常处理方面有一定简化。由于 AOT 编译无法预知所有的异常路径,Mono AOT 采用了"setjmp/longjmp + 信号处理"的异常处理模式,这种模式相比 JIT 模式下的异常处理增加了额外的开销。特别是在 iOS 上,Mono AOT 的异常处理需要与 iOS 的 Mach Exception Handling 机制交互,异常路径的性能损失更为明显。

不能处理动态生成的代码。这是所有 AOT 方案的共性局限。Mono AOT 的编译产物是静态的、封闭的,无法在运行时加载和执行新的 IL 代码——即使这份代码来自开发者自己编写的热更新 DLL。这也是为什么所有在 Mono AOT 时代上线的 Unity 游戏,要实现"热更新"功能,都必须借助 Lua 脚本或纯 C# 解释器等额外方案的原因。


五、Mono 与 HybridCLR 的关系

5.1 HybridCLR 是否依赖 Mono(答案:不依赖)

这是 HybridCLR 技术架构中最常被误解的问题之一。明确回答:HybridCLR 完全不依赖 Mono 运行时

许多开发者在初次了解 HybridCLR 时,会本能地认为"HybridCLR 是一个 Mono 解释器,挂载到了 IL2CPP 上"。这个理解是错误的。HybridCLR 的核心运行时(hybridclr 仓库)是基于 IL2CPP 的运行时 API 和数据结构开发的全新 C++ 代码,它直接在 IL2CPP 的内存模型和类型系统之上运行,与 Mono 没有任何代码依赖关系。

具体的证据包括:

  • GC 依赖:HybridCLR 的热更新代码使用 IL2CPP 的 GC(Unity GC),而非 Mono 的 SGen GC。热更新代码中分配的对象由 IL2CPP 的 GC 统一管理,遵循 IL2CPP 的 GC 策略(包括增量式 GC)。

  • 类型系统:HybridCLR 的热更新类型注册到 IL2CPP 的元数据系统中,使用 IL2CPP 的类型布局算法计算内存布局。热更新对象与 AOT 对象在内存中的布局完全兼容,不需要 Mono 运行时参与。

  • 方法调用:HybridCLR 使用 IL2CPP 的虚函数表(vtable)和函数指针调用机制实现 AOT 与热更新代码的互调。当 AOT 代码调用热更新方法时,调用路径完全经过 IL2CPP 运行时,没有 Mono 介入。

  • 平台代码:HybridCLR 的代码库不包含任何 Mono 源码,也不需要在运行时加载任何 Mono 的动态库。

之所以会产生"HybridCLR 依赖 Mono"的误解,可能是因为 HybridCLR 的 Interpreter 模式和 Mono 的 Interpreter 模式在设计思想上有相似之处。但这只是"殊途同归"——两种运行时都采用了"AOT + Interpreter"混合执行的概念,但具体实现完全独立。

5.2 HybridCLR 的运行时与 Mono 的差异

从架构层面来看,HybridCLR 的运行时与 Mono 运行时存在一系列本质性差异:

维度 Mono 运行时 HybridCLR 运行时
宿主运行时 独立运行时 嵌入在 IL2CPP 中
类型系统 Mono 自身的类型系统 IL2CPP 的类型系统(扩充)
GC SGen GC 或 Boehm GC IL2CPP 的 Unity GC
代码执行方式 JIT / AOT / Interpreter AOT(IL2CPP)+ Interpreter(HybridCLR)
方法编译 JIT 编译器(运行时) HybridCLR 编译器(热更新 DLL 加载时)
指令集 IL 指令(对于解释器) HybridCLR 自定义寄存器指令
异常处理 Mono 异常处理机制 IL2CPP 异常处理机制
元数据存储 运行时加载的 DLL 元数据 IL2CPP MetadataCache + 动态注册
平台支持 所有 .NET 支持的平台 所有 IL2CPP 支持的平台

最重要的区别是 运行时定位。Mono 是一个完整的、自包含的 .NET 运行时,它不需要依赖其他运行时即可执行 C# 代码。HybridCLR 则不是完整的运行时——它只是 IL2CPP 的一个增强模块,不能脱离 IL2CPP 独立工作。HybridCLR 本身不提供内存管理(GC)、线程调度、类型解析等基础设施,这些全部由 IL2CPP 运行时提供。

在 代码执行路径 上,两者的差异更加明显。Mono Interpreter 直接执行 IL 指令,其解释器是通用的、与 JIT 共享同一套元数据体系。HybridCLR 则是一个两步过程:首先由编译器模块将 IL 翻译为自定义寄存器指令,然后由解释器模块执行这些寄存器指令。HybridCLR 的自定义寄存器指令集经过专门设计,指令密度更高(一条寄存器指令可能对应多条 IL 指令),减少了解释器的指令解码开销。

5.3 HybridCLR 对 Mono 混合执行模式的借鉴

尽管 HybridCLR 不依赖 Mono 的代码,但在设计思想上,HybridCLR 与 Mono 的混合执行模式(Mixed Mode Execution)有着明显的传承关系。

Mono 混合执行模式的核心概念是:运行时中同时存在解释器和 JIT 编译器,冷代码使用解释器,热代码使用 JIT 编译器。这种设计的理论基础是"二八定律"——在大多数应用程序中,80% 的执行时间花在 20% 的代码上(热点代码)。为冷代码使用快速的解释器执行可以减少不必要的 JIT 编译开销(编译时间 + 代码缓存占用),而为热代码使用 JIT 编译器可以确保热点路径的高性能。

HybridCLR 借鉴了类似的思路,但在实现上下文上做了调整。在 HybridCLR 的场景中,AOT 代码已经是编译好的原生代码(对应 Mono 中的 JIT 编译代码),热更新代码走解释器路径(对应 Mono 中的解释器)。HybridCLR 的 DHE 技术进一步将这个模型推向了极致——即使在热更新代码内部,也只有发生了变更的函数走解释器路径,未变更的函数仍然以 AOT 方式运行。

两种模式的对比:

Mono 混合执行模式:
  IL 代码 → 解释器执行(冷代码)→ 热代码 → JIT 编译 → 机器码执行
                                          ↑
                               调用计数器达到阈值切换

HybridCLR 混合执行模式:
  AOT 代码 → 机器码直接执行(始终 AOT)
  热更新代码 → HybridCLR 编译器 → 寄存器指令 → 解释器执行(首次变更)
           └── 未变更函数 → AOT 机器码执行(DHE 优化)

可以看到,HybridCLR 在本质上实现了与 Mono 混合执行模式相同的目标——在运行时中同时存在高效的预编译代码和灵活的解释执行代码。区别在于,Mono 两者都在同一个运行时内,而 HybridCLR 的 AOT 代码来自 IL2CPP 编译,解释器代码来自 HybridCLR 模块注入。

5.4 从 Mono 到 IL2CPP 再到 HybridCLR 的演进

Unity 脚本后端的演进史,实际上是一部"在 AOT 平台限制下不断优化 C# 执行效率"的技术史。理解这条演进脉络,有助于把握 HybridCLR 在整个技术谱系中的位置。

Mono 时代(2005-2014):Unity 使用 Mono 作为唯一脚本后端。在支持 JIT 的平台上(桌面、Android),Mono 以 JIT 模式运行,提供良好的运行时性能和完整的 .NET API 支持。在不支持 JIT 的平台上(iOS),Mono 以 Full AOT 模式运行,遭受泛型支持不完整、反射受限、包体积偏大等问题。这一时期,Unity 开发者面临一个隐形的"平台税"——同一套代码在 Android 上运行良好,在 iOS 上可能因为 Mono AOT 的限制而崩溃。

IL2CPP 时代(2015-2019):Unity 研发并推广 IL2CPP。IL2CPP 通过 C++ 中间层绕过了 Mono 的 AOT 编译器质量问题,利用 LLVM/clang 的成熟优化能力生成更高效的机器码。同时,IL2CPP 的 Managed Code Stripping 机制大幅减少了包体积。IL2CPP 的推广是一个渐进的过程——从 iOS 开始,逐步覆盖 Android、Windows、macOS 等所有主要平台。到 Unity 2019 LTS 版本,IL2CPP 已成为全平台默认编译后端。但 IL2CPP 面临着一个新的问题:它比 Mono AOT 更加"封闭"——Mono AOT 至少还在运行时保留了部分运行时基础设施(如反射元数据),而 IL2CPP 将几乎所有元数据都序列化为静态的 C++ 数据结构,完全不支持运行时的动态代码加载。

HybridCLR 时代(2022-至今):HybridCLR 在 IL2CPP 的封闭体系中打开了一个突破口。它不推翻 IL2CPP 的 AOT 基础,而是在这个基础上叠加了 Interpreter 模块,使得 IL2CPP 从纯 AOT 运行时进化为 "AOT + Interpreter" 混合运行时。HybridCLR 的出现,本质上解决了 Unity 多年来在 AOT 与动态性之间的根本矛盾——开发者不再需要在"使用 IL2CPP 获得高性能"和"保留动态代码执行能力实现热更新"之间做取舍。

这三个阶段的演进特征可以概括为:

阶段 运行时 动态性 性能 热更新能力
Mono 时代 Mono(JIT/AOT) 高(JIT 平台)/ 低(AOT 平台) 依赖额外方案(Lua)
IL2CPP 时代 IL2CPP 极低(纯 AOT) 依赖额外方案(Lua/ILRuntime)
HybridCLR 时代 IL2CPP + HybridCLR 中等(解释器模式) 极高(AOT)+ 中等(解释器) 原生 C# 热更新

从演进趋势来看,Unity 的脚本后端不断朝着 更高性能 和 更好动态性 两个方向同时发展。Mono 牺牲了 AOT 平台的性能换取动态性,IL2CPP 牺牲了动态性换取性能,而 HybridCLR 试图在两者之间找到最佳平衡——用 AOT 保证绝大多数代码的高性能执行,用解释器为热更新代码提供动态性。


六、Mono 的最新发展

6.1 Mono 作为 .NET 6/7/8 的一部分

2020 年 11 月,微软宣布将 Mono 运行时整合到 .NET 平台统一计划中。这意味着 Mono 不再是独立于 .NET Core 的"另一个 .NET 实现",而是成为统一 .NET 平台在移动端、WebAssembly 和桌面端的重要组件。

在 .NET 6 及后续版本中,Mono 的角色被重新定义为 面向移动端和 WebAssembly 场景的 .NET 运行时。具体来说:

  • MAUI(Multi-platform App UI):.NET MAUI 使用 Mono 运行时作为 Android、iOS、macOS 等平台上的默认运行时。MAUI 选择 Mono 而非 .NET Core CLR 的原因很实际——Mono 已经在这些平台上经过了十多年的生产验证,拥有成熟的跨平台集成经验。

  • WebAssembly:Mono 的 WebAssembly 支持(MonoWasm)在 .NET 6/7/8 中被深度整合。Blazor WebAssembly 使用 Mono 运行时在浏览器中执行 .NET 代码。在 .NET 8 中,Mono 的 WebAssembly 支持引入了 AOT 编译和现代化的线程支持。

  • 统一运行时层:在 .NET 6+ 中,Mono 和 .NET Core CLR 共享了大量的基础设施,包括 GC(移除了 SGen,改用 .NET Core 的 GC)、JIT(部分场景使用 .NET Core 的 JIT)和 BCL(基类库统一)。这使得两者的差异进一步缩小。

.NET 统一计划对 Mono 的影响是深远的。一方面,Mono 的技术债务得以清偿——它不再需要独立维护一套 GC、JIT 和 BCL,可以直接利用 .NET Core 的成熟实现。另一方面,Mono 的品牌和作为独立运行时的身份正在淡化——它越来越多地被视为"统一 .NET 运行时在特定平台上的适配层"。

6.2 Unity 对 Mono 的长期策略

对于 Unity 来说,Mono 的未来角色已经非常明确:Editor 中的开发辅助运行时,但不再是生产发布的推荐选择

在 Unity 2023 LTS 和 Unity 6000 中,Mono 仍然是 Editor 模式的默认脚本后端。这是因为 Mono 的 JIT 模式提供了最佳的开发迭代体验——修改代码后即时生效,不需要经历 IL2CPP 的全量编译流程。Mono 在 Editor 中的角色类似于一种"开发模式加速器",它让开发者能够快速迭代,同时在调试时提供完整的栈跟踪和异常信息。

但在生产发布方面,Unity 的策略是明确推进 IL2CPP 的全面采用。Unity 官方文档中明确指出:对于新项目,推荐使用 IL2CPP 作为默认的脚本后端。未来版本的 Unity 可能进一步限制 Mono 在生产发布中的支持范围,甚至最终将 Mono 从发布模式中完全移除。

Unity 对 Mono 的长期维护策略是"保持兼容,最小化维护"——Unity 会继续支持 Mono 作为 Editor 的运行时,会修复影响 Editor 使用体验的问题,但不会在 Mono 上进行重大的性能改进或特性增强。Unity 的研发资源已经全面转向 IL2CPP 和 DOTS(Data-Oriented Technology Stack)技术栈。

这一策略对 HybridCLR 的影响是:HybridCLR 的选择——增强 IL2CPP 而非依赖 Mono——是与 Unity 的未来方向完全一致的。如果 HybridCLR 选择了基于 Mono 的技术路线,它将面临与 Unity 自身战略方向背道而驰的风险——这是一条注定被淘汰的道路。

6.3 Mono 在游戏开发中的未来

从游戏开发的视角来看,Mono 的未来角色需要放在更广阔的技术图景中审视。

短期内(1-3 年):Mono 将继续作为 Unity Editor 的开发运行时存在。对于使用 HybridCLR 的团队,Mono 负责 Editor 模式下的调试和迭代,IL2CPP + HybridCLR 负责发布模式的性能表现和热更新能力。Mono 的 SGen GC 在评测场景中可能仍然是性能分析的参考标杆,但不再是生产环境的选择。

中期(3-5 年):Unity 可能进一步降低 Mono 在发布模式下的重要性,甚至完全不推荐在生产构建中使用 Mono。IL2CPP 将成为 Unity 唯一的官方支持的发布运行时。原生 AOT 编译(Native AOT,如 .NET 的 Native AOT 和 IL2CPP 的 C++ 代码生成)在游戏开发中将更加普及。Mono 在游戏开发中的角色将收缩到纯粹的开发工具层面。

长期(5 年以上):随着 WASM(WebAssembly)技术在游戏领域的渗透,Mono 在 WebAssembly 上的实现(MonoWasm)可能找到新的应用场景。云游戏的兴起也为 Mono 提供了新的舞台——在服务器端,Mono 可以作为一种"轻量级游戏逻辑沙箱"使用。但这些场景将更多地发生在 Unity 生态之外,而非 Unity 引擎内部。

对于 HybridCLR 使用者而言,理解 Mono 的演进趋势有助于建立正确的技术判断:Mono 是历史,IL2CPP 是现在,HybridCLR 是基于 IL2CPP 的未来扩展。在技术选型时,凡是依赖 Mono 运行时特性的方案(如直接修改 Mono 源码、使用 Mono 的私有 API 等)都面临与 Unity 战略方向不匹配的风险。HybridCLR 选择在 IL2CPP 上做扩展而非修改 Mono,恰恰是其设计前瞻性的体现。


总结

本文系统性地剖析了 Mono 运行时的架构、IL 执行模型、GC 机制、AOT 模式,以及它与 HybridCLR 的深层关系。

核心要点回顾

  1. Mono 的架构三要素:运行时核心层(类型系统与元数据管理)、GC 层(SGen 分代 GC)、编译后端层(JIT/AOT/Interpreter)。这三层协作提供了完整的 .NET 运行时能力。

  2. IL 执行模型:Mono 基于评估栈的 IL 执行模型是 ECMA-335 规范的标准实现。JIT 编译将栈式 IL 转换为寄存器操作,解释器模式则直接逐条执行 IL。Mono 5.0 引入的混合执行模式(Interpreter + JIT 动态切换)与 HybridCLR 的设计理念有着直接的渊源关系。

  3. GC 机制差异:Mono 的 SGen 分代 GC 与 IL2CPP 的 Unity GC 在代际策略、写屏障实现、增量 GC 支持等方面存在差异。HybridCLR 使用 IL2CPP 的 GC,这是它不依赖 Mono 的重要证据之一。

  4. Mono AOT 的局限:泛型支持不完整、反射受限、代码生成质量不如 LLVM,这些局限直接推动了 Unity 从 Mono AOT 向 IL2CPP 的迁移。

  5. HybridCLR 不依赖 Mono:这是理解 HybridCLR 技术架构的关键。HybridCLR 是 IL2CPP 的增强模块,使用 IL2CPP 的类型系统、GC 和运行时基础设施。两者的设计理念有相似之处,但实现完全独立。

  6. 从 Mono 到 IL2CPP 再到 HybridCLR 的演进:这一演进体现了 Unity 脚本后端在"高性能"和"动态性"之间不断寻找平衡的过程。HybridCLR 是这一演进的最新成果——在保持 IL2CPP 高性能的同时,通过 Interpreter 模块提供了动态代码执行能力。

下一篇预告:在理解了 Mono 运行时之后,我们将进入对比篇——第 06 篇将深度解析 ILRuntime 的原理、优势和劣势。ILRuntime 是 HybridCLR 出现之前最流行的纯 C# 热更新方案,理解它的技术设计有助于全面评估 HybridCLR 的技术先进性。


参考资源

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐