Unity内C#脚本的编译运行

25 年 4 月 1 日 星期二
1310 字
7 分钟

本文概述了 Unity 中 C# 脚本可能经历的不同编译与运行流程。有待进一步整理润色。


一、常规 .NET 环境下的 C# 编译执行(对比参考)

在开始讨论 Unity 之前,先看一下最常见的 .NET 程序是如何从 C# 源码变成可以运行的机器码的(以 .NET 6/.NET Framework 等为例):

mermaid
flowchart LR
    A["C# 源代码"] --> B["C# 编译器 (Roslyn 等)<br/>生成 IL (中间语言)"]
    B --> C[".dll / .exe<br/>(IL + 元数据)"]
    C --> D["JIT(Just In Time 编译) 或 AOT 编译"]
    D --> E["最终的机器码 (01010101)"]
  • C# 源代码:就是我们日常写的 .cs 脚本。
  • 编译器:将 .cs 编译为 IL(Intermediate Language),并存储在 .dll.exe 文件。
  • JIT / AOT:在运行时(或在发布时),由 CLR 或其他运行时环境将 IL 转换为机器码。
  • 机器码:最终在操作系统和 CPU 上执行。

这个流程在 Unity 的编辑器里,也或多或少有类似之处,只是 Unity 在发布构建时会有更多处理。


二、Unity 编辑器模式下的脚本执行(Mono)

当你在 Unity 编辑器 中按下 “Play” 时,大多数时候脚本是通过 Mono(或可选的 .NET Core 体验)来执行的,以便实现快速开发、调试、热重载等需求。

mermaid
flowchart LR
    A["C# 源代码"] --> B["Unity 自带编译器 (基于 Roslyn)<br/>生成 IL (DLL)"]
    B --> C["Mono 运行时 (JIT)"]
    C --> D["即时生成并执行机器码"]
  • 编辑器下,Unity 并不会做复杂的 AOT 转换,为的是让开发者能快速迭代、打断点调试。
  • 如果项目中启用了 Burst Compiler,则标记了 [BurstCompile] 的方法依旧会由 Burst 专门编译出更高性能的机器码(详见后文)。

三、发布构建时的 IL2CPP 工作原理

一旦我们要真正发布项目(尤其是移动设备或主机平台),Unity 通常会启用 IL2CPP。这是 Unity 提供的“IL -> C++ -> 机器码”的 AOT 编译流程。大致可以分为以下步骤:

mermaid
flowchart LR
    C0["C# 源代码"] --> C1["编译器生成 IL (DLL)"]
    C1 --> C2["IL2CPP: 将 IL 转换成 C++ 代码"]
    C2 --> C3["使用平台原生 C++ 编译器<br/>编译为目标机器码"]
    C3 --> C4["可执行文件 / 动态库"]
  • IL2CPP 会分析所有脚本生成的 IL,将其转成一堆对应的 C++ 源文件。
  • 最后,再由平台自带的编译器(如 Clang、MSVC、Android NDK 等)把 C++ 编译为可执行文件或库。
  • 对于很多限制 JIT 的平台(如 iOS)或游戏主机,这是必然选择。

AOT 与反射

即使走了 AOT,一些反射功能仍然可用,只要相关元数据没有被裁剪(Stripping)。Unity 会在构建时把必要的元数据打包进去,以支持 Type.GetType, MethodInfo.Invoke 等常见的反射用法。但像 System.Reflection.Emit 这种动态生成代码的操作,在 AOT 环境下就行不通了。


四、Burst Compiler:在关键代码上进一步优化

Burst Compiler 是 Unity 针对 Job System / DOTS / ECS 高并发场景下的高性能编译器。它可以直接从 IL 生成深度优化的机器码(包括 SIMD、向量化等),提升运算密集型逻辑的速度。它与 Mono 或 IL2CPP 并不冲突,而是并行存在

  1. 编辑器(Mono) 下运行时,如果方法标记了 [BurstCompile],则在运行时可由 Burst 编译器编译成机器码;如果 Burst 编译失败或不启用,则回退到 Mono JIT 的托管版本。
  2. IL2CPP 构建中,带 [BurstCompile] 的代码同样会由 Burst 做“IL -> 机器码”的转换,然后与 IL2CPP 转换的代码一同打包进最终可执行文件。Job System 在调度时会直接调用 Burst 优化后的函数指针。

简化的流程可用下图展示(以编辑器下 Mono + Burst 为例):

mermaid
flowchart LR
    B0["C# 源码含<br/>[BurstCompile]方法"] --> B1["编译器 -> IL(DLL)"]
    B1 --> B2["Burst 编译器<br/>(针对特定方法)"]
    B2 --> B3["生成优化后的机器码"]
    B1 --> B4["Mono JIT<br/>(处理其余代码)"]
    B4 --> B5["机器码 (常规)"]
    B3 --> B6["Job System 调度<br/>高性能函数指针"]
    B5 --> B6

最终运行时,非 Burst 方法走 Mono JIT 的机器码,Burst 方法则调用专门优化过的那部分机器码。


五、总结

  • 编辑器模式(Mono 或 .NET Core):C# 脚本以常规 IL 形式编译后,通过 Mono JIT(或 .NET Core)运行,方便开发调试。
  • IL2CPP:发布构建时,把所有 IL 转为 C++ 并再编译为目标机器码,适配各种平台并满足 AOT 需求。
  • Burst Compiler:对有 [BurstCompile] 标记的代码进行更高强度的优化编译,可与 Mono 或 IL2CPP 同时使用,从而提升数据并行任务的性能。
  • 反射 等元数据相关功能,在 AOT 环境下仍旧可用,只要确认编译时没有被裁剪。

上述流程组合在一起,就形成了Unity 在不同阶段、不同平台上对 C# 脚本编译与运行的灵活方案:编辑器开发时尽量高效迭代,发布时尽量兼容性好并尽量获取更高性能,而对于关键性能区域(Jobs/ECS),再借助 Burst 来让你的游戏或应用运行得更快。

文章标题:Unity内C#脚本的编译运行

文章作者:DWHITE

文章链接:https://dr9k69ai79.github.io/MyBlog/posts/unity_csharp脚本的编译运行[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。