Unity 是一个基于 .NET 框架的游戏引擎。在 Unity 中使用 C# 作为脚本语言,可以使用 .NET 的功能和类库进行开发。

目前,Unity 有两种运行时,分别是 Mono 和 IL2CPP。

  • Mono 是 Unity 早期使用的运行时,它基于跨平台的 Mono 框架,支持 Windows、macOS 和 Linux。Mono 使用即时编译(JIT)的方式,在运行时将 C# 代码编译为机器码。
  • IL2CPP 是 Unity 引入的另一种运行时,采用预编译(AOT)的方式,在构建时将 C# 代码编译为 C++ 代码,再生成可执行文件或者 DLL。

两种方式各有优劣。Mono 的开发迭代和调试效率更高,但运行时性能较差;IL2CPP 的编译时间过长,但运行时性能较好。因此,策略上可以在开发阶段使用 Mono,而在发布阶段使用 IL2CPP。

托管代码剥离

在构建时,Unity 会编译项目中的程序集,并可以检测和删除未使用的代码。这个过程会增加构建时间,但会减少最终的构建包体积。

当使用 Mono 时,代码剥离默认处于禁用状态;当使用 IL2CPP 时,代码剥离无法被禁用。在 Edit > Project Settings > Player > Other Settings > Optimization 中可以设置代码剥离的级别。这个值越高,删除的代码越多,构建时间越长。

Unity 可能会误删代码,比如一些反射调用的代码。可以在代码上添加 Preserve 特性来阻止代码被删除。

垃圾收集机制

Unity 使用著名的 [Boehm GC] 作为垃圾收集器,默认使用增量模式。在增量模式下,GC 会将收集垃圾的过程分散到多个帧上,以减少卡顿和 CPU 峰值。在 Edit > Project Settings > Player > Other Settings 中,去掉 Use incremental GC 的勾选可以禁用增量 GC 模式。

代码反射开销 ⭐

Mono 和 IL2CPP 都在内部缓存了所有 C# 的反射对象,并且,这些对象不会被垃圾收集。因此,在运行过程中,GC 会不断扫描到这些反射对象,导致不必要的开销。

为了减少这部分开销,应该尽量避免使用反射方法。可以在编辑器中将所需数据序列化以供运行时使用。

异步编程

Unity 提供了内置的 Await 支持,在多数情况下,应该使用 Awaitable 而不是 Task。

Unity 的大多数 API 都不是线程安全的,引擎本身还使用了自定义的 UnitySynchronizationContext 来覆盖默认的 SynchronizationContext,保证默认情况下所有 Task 都在主线程上执行。当明确需要在后台线程上执行时,可以使用以下方式来实现。

private async Awaitable<float> CalculateAsync()
{
    await Awaitable.BackgroundThreadAsync();
    var result = 0f;
    return result;
}

private async void Start()
{
    var result = await CalculateAsync();
}

需要注意,当退出 Play 模式时,Unity 不会自动停止后台线程,需要使用 Application.exitCancellationToken

多线程

使用 Unity 提供的 Job system 来实现。