Version: 2020.3
Unity 架构
.NET 配置文件支持

Unity 中的 .NET 概述

Unity 使用开源 .NET 平台,以确保使用 Unity 创建的应用程序可以在各种不同的硬件配置上运行。.NET 支持一系列语言和 API 库。

脚本后端

Unity 具有两个脚本后端 Mono 和 IL2CPP (Intermediate Language To C++),它们各自使用不同的编译技术:

  • Mono 使用即时 (JIT) 编译,在运行时按需编译代码。
  • IL2CPP 使用提前 (AOT) 编译,在运行之前编译整个应用程序。

使用基于 JIT 的脚本后端的好处是编译时间通常比 AOT 快得多,而且它与平台无关。

Unity 编辑器基于 JIT,使用 Mono 作为脚本后端。为应用程序生成播放器时,可以选择要使用的脚本后端。要通过编辑器执行此操作,请转到 Edit > Project Settings > Player,打开 Other Settings 面板,然后单击 Scripting Backend 下拉列表并选择所需后端。

托管代码剥离

生成应用程序时,Unity 会扫描已编译的程序集 (.DLL) 以检测并移除未使用的代码。此过程会减少生成的最终二进制文件大小,但会增加生成时间。

使用 Mono 时,在默认情况下会禁用代码剥离,但无法为 IL2CPP 禁用代码剥离。可以控制 Unity 在剥离代码时的激进程度。转到 Edit > Project Settings > Player,打开 Other Settings 面板,然后单击 Managed Stripping Level 下拉列表并选择所需的代码剥离级别。有关代码剥离的更多信息,请参阅托管代码剥离文档。

注意:代码剥离在某些情况下可能过于激进,可能会移除所依赖的代码,尤其是在使用反射时。可以使用 Preserve 属性和 link.xml 文件来防止剥离特定类型和函数。

垃圾收集

Unity 将 Boehm 垃圾回收器用于 Mono 和 IL2CPP 后端。Unity 在默认情况下使用增量模式。可以禁用增量模式以使用“可停止所有工作”的垃圾收集,不过 Unity 建议使用增量模式。

To toggle between Incremental mode and “stop the world”, go to Edit > Project Settings > Player, open the Other Settings panel and click the Use incremental GC checkbox. In Incremental mode, Unity’s garbage collector only runs for a limited period of time and does not necessarily collect all objects in one pass. This spreads the time it takes to collect objects over a number of frames and reduces the amount of stuttering and CPU spikes. For more information, see Understanding Automatic Memory Management.

要检查应用程序中的分配和可能 CPU 峰值的数量,请使用 Unity 性能分析器。还可以使用 GarbageCollector API 在播放器中完全禁用垃圾收集。禁用回收器后,应该小心避免分配过多的内存。

.NET 系统库

Unity 支持许多平台,可能会根据平台使用不同的脚本后端。在某些情况下,.NET 系统库需要特定于平台的实现才能正常工作。尽管 Unity 尽最大努力支持尽可能多的 .NET 生态系统,但对于 Unity 明确不支持的部分 .NET 系统库,也有一些例外。

Unity 不保证 .NET 系统库在 Unity 版本间的性能和分配。作为一般经验法则,Unity 不会修复 .NET 系统库中的任何性能退化。

Unity 不支持 System.Drawing 库,并且不保证在所有平台上都正常工作。

JIT 脚本后端允许在应用程序运行时发出动态 C#/.NET 中间语言 (IL) 代码生成,而 AOT 脚本后端不支持动态代码生成。使用第三方库时考虑到这一点十分重要,因为它们对于 JIT 和 AOT 可能具有不同的代码路径,或者它们可能会使用依赖于动态生成代码的代码路径。有关如何在运行时生成代码的更多信息,请参阅 Microsoft 的 ModuleBuilder 文档。

尽管 Unity 支持多个 .NET API 配置文件,不过应该对于所有新项目都使用 .NET Standard 2.0 API 兼容性级别,原因如下:

  • .NET Standard 2.0 是较小的 API 表面,因此具有较小的实现。这会减小最终可执行文件的大小。
  • .NET Standard 2.0 具有更好的跨平台支持,因此代码更有可能跨所有平台正常工作。
  • .NET Standard 2.0 受所有 .NET 运行时支持,因此代码可以跨更多 VM/运行时环境(例如 .NET Framework、.NET Core、Xamarin、Unity)正常工作。
  • .NET Standard 将更多错误移至编译时。.NET 4.7.1 中的一些 API 可在编译时使用,但在某些平台上的实现会在运行时抛出异常。

例如,如果需要为较旧的现有应用程序提供支持,则其他配置文件可能会很有用。如果需要不同的 API 兼容性级别,请在 Player Settings 中更改 .NET Profile。要执行此操作,请转到 Edit > Project Settings > Player > Other Settings,然后从 Api Compatibility Level 下拉菜单中选择所需级别。

使用第三方 .NET 库

应该只使用在各种 Unity 配置和平台上经过广泛测试的第三方 .NET 库。

注意:第三方库中 JIT 和 AOT 代码路径的性能特征可能会显著不同。AOT 通常会减少启动时间,因此适用于较大的应用程序,但会增加二进制文件大小以容纳已编译代码。AOT 在开发过程中也需要较长时间进行生成,无法更改已编译代码的行为以针对任何特定平台。JIT 在运行时会基于它运行的平台进行调整,这可以提高运行性能,但代价是可能会延长应用程序启动时间。因此,应该在编辑器和目标平台上对应用程序进行性能分析。有关更多信息,请参阅 Unity 性能分析器文档。

应该在所有目标平台上对 .NET 系统库的使用情况进行性能分析,因为其性能特征可能会因所使用的脚本后端、.NET 版本和配置文件而异。

查看第三方库时,请考虑以下方面:

  • 兼容性:第三方库可能与某些 Unity 平台和脚本后端不兼容。
  • 性能:与其他 .NET 运行时相比,第三方库在 Unity 中可能具有截然不同的性能特征。
  • AOT 二进制文件大小:由于库使用的依赖项数量,第三方库可能会显著增大 AOT 二进制文件大小。

C# 反射开销

Mono 和 IL2CPP 会在内部缓存所有 C# 反射 (System.Reflection) 对象,并且按照设计,Unity 不会对它们进行垃圾收集。此行为的结果是垃圾回收器在应用程序生命周期内持续扫描缓存的 C# 反射对象,这会导致不必要和潜在的大量垃圾回收器开销。

要最大程度减少垃圾回收器开销,请在应用程序中避免使用诸如 Assembly.GetTypesType.GetMethods() 等方法,这些方法会在运行时创建许多 C# 反射对象。而是应该在编辑器中扫描程序集以获取所需数据,并进行序列化和/或代码生成以在运行时使用。

UnityEngine.Object 特殊行为

UnityEngine.Object 是 Unity 中的一种特殊类型的 C# 对象,因为它链接到原生 C++ 对应对象。例如,使用 Camera 组件时,Unity 不会将对象的状态存储在 C# 对象中,而是存储在其原生 C++ 对应对象中。

Unity 目前不支持将 C# WeakReference 类与 UnityEngine.Object 一起使用。因此,不应使用 WeakReference 引用加载的资源。有关 WeakReference 类的更多信息,请参阅 Microsoft 的 WeakReference 文档

Unity C# 和 Unity C++ 共享 UnityEngine 对象

使用诸如 Object.DestroyObject.DestroyImmediate 等方法销毁 UnityEngine.Object 派生对象时,Unity 会销毁(卸载)原生对应对象。无法使用显式调用销毁 C# 对象,因为垃圾回收器会管理内存。一旦不再引用托管对象,垃圾回收器便会收集并销毁它。

如果再次访问已销毁的 `UnityEngine.Object,则 Unity 会为大多数类型重新创建原生对应对象。此重新创建行为的两个例外是 MonoBehaviourScriptableObject:一旦被销毁,Unity 便绝不会重新加载它们。

MonoBehaviour 和 ScriptableObject 会覆盖相等 (==) 和不相等 (!=) 运算符。因此,如果将销毁的 MonoBehaviour 或 ScriptableObject 与 null 进行比较,则当托管对象仍然存在且尚未进行垃圾收集时,运算符会返回 true。

因为 ???. 运算符不可重载,所以它们与从 UnityEngine.Object 派生的对象不兼容。在托管对象仍然存在的情况下对销毁的 MonoBehaviour 或 ScriptableObject 进行使用时,这些运算符不会返回与相等和不相等运算符相同的结果。

避免使用异步和等待

Unity API 不具有线程安全性,因此不应使用异步和等待任务。异步任务通常在调用时分配对象,这在过度使用它们时可能会导致性能问题。此外,退出运行模式时,Unity 不会自动停止在托管线程上运行的异步任务。

Unity 会使用自定义 UnitySynchronizationContext 覆盖默认 SynchronizationContext,在编辑播放模式下对主线程运行所有任务。要使用异步任务,必须使用 TaskFactory 手动创建和处理自己的线程,以及使用默认 SynchronizationContext 而不是 Unity 版本。要监听进入和退出运行模式事件以手动停止任务,请使用 EditorApplication.playModeStateChanged。但是,如果采用这种方法,则大多数 Unity 脚本 API 都无法使用,因为未使用 UnitySynchronizationContext。

Unity 架构
.NET 配置文件支持