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 来实现。