Unity .Net 学习日志⚓︎
基于官方文档 - Overview of .NET in Unity 学习,记录一些使用到的内容。Overview of .NET in Unity
脚本后端⚓︎
分为即时编译(JIT - just in time)和预先编译(AOT - ahead of time)
JIT 是在运行时按需编译代码,适用于动态语言。
AOT 是在运行之前编译整个应用程序。
Unity 可以选择两种脚本后端,Mono 和 IL2CPP。
Mono 是 JIT 模式,IL2CPP 是 AOT 模式。使用基于 JIT 的脚本后端编译通常比基于 AOT 的脚本后端快得多。
托管代码剥离⚓︎
使用 Mono 默认情况下禁用代码剥离。IL2CPP 默认启用,且不能禁用。
垃圾回收⚓︎
Unity 将 Boehm 的垃圾回收器用于所有脚本后端。Unity 建议使用 Incremental 垃圾回收。
可以完全禁用垃圾回收,使用 GarbageCollector API 完全禁用。
.NET
系统库⚓︎
Mono 脚本后端使用 JIT 编译,能够在运行时动态C#或者IL代码生成。IL2CPP 使用的 AOT 不支持动态代码生成。
所有 .NET 运行时都支持 .NET Standard
,因此使用 .NET Standard
兼容性更好。
.NET Standard
将更多错误移动到编译时,在编写代码时就会发现错误。.NET Framework
的许多 API 在编译时可用,但是在某些平台的运行时出现异常。
C# 反射开销⚓︎
Mono 和 IL2CPP 都支持反射,并且反射时会生成 C# 反射对象,根据 Unity 设计,反射对象不会被垃圾回收器回收,因此,在程序运行时,所有的反射对象都会持续存在内存中,这可能会导致内存泄漏。而且也会增加垃圾回收器的开销,因为垃圾回收时会检测所有对象。
为了最大程度地减少垃圾回收器的开销,避免在运行时使用 Assemly.GetTypes 和 Type.GetMethods 等方法来获取类型信息,这些方法会创建大量反射对象,所以应该在编辑器阶段使用,然后对其序列化或者编码生成,以便在运行时使用。
Unity 提供了 TypeCache 类,该类提供了缓存类型信息的功能,可以提高性能。TypeCache 类位于 UnityEditor 程序集。
UnityEngine.Object 的特殊行为⚓︎
UnityEngine.Object 类是所有 Unity 对象的基类,是一个特殊类型的 C# 对象,它会链接到 Native 的 C++ 对应的对象。相当于一个桥梁。例如 Camera 组件,Unity 会将对象的状态存储在 Native 的 C++ 对象中,而不是 C# 对象。可以简单理解为 UnityEngine.Object 类会提供对这个对象状态的访问。
Unity 目前不支持 C# WeakReference 和 UnityEngine.Object 实例一起使用。不应该使用 WeakReference 来引用加载的资产。
Unity C# 和 Unity C++ 共享 UnityEngine.Object⚓︎
当我们使用 Object.Destroy() 或者 Objecet.DestroyImmediate() 销毁派生自 UnityEngine.Object 的对象时,Unity 会销毁(卸载)Native Counter Object。(这个通常被认为是某种标记?)
我们不能直接显式销毁 C# 对象,因为有垃圾回收器管理内存,一旦托管对象不再拥有任何引用,这个垃圾回收器将会收集并销毁它。
如果应用程序(通常就是指运行时),尝试访问已经被销毁的 UnityEngine.Object 对象,Unity 会为大多数类型重新创建 Native Conterpart Object. 但是有两个例外:MonoBehaviour 和 ScriptableObject。他们一旦被销毁,Unity 就永远不会重新加载它们。
MonoBehaviour 和 ScriptableObject 会重载 ==
和 !=
操作符,如果执行了 Destory 的操作,将已经销毁的 MonoBehaviour 和 ScriptableObject 对象和 C# 的 null 直接进行比较,则当托管对象仍然存在且尚未进行垃圾回收时,将返回 true。和上文类似,首先销毁的是一个 Native Counter Object,然后等待垃圾回收器去收集并销毁实际的 C# 对象。
因为无法重载 ??
和 ?.
操作符,所以无法进行重载设计,所以无法与派生自 UnityEngine.Object 的兼容。意思是当我们对已经执行过 Destroy,但是托管对象(C# 对象)仍然存在的 MonoBehaviour 和 ScriptableObject 对象使用这两个运算符时,这些运算符不会返回与 ==
和 !=
运算符相同的结果。
async 和 await tasks 的限制⚓︎
Unity 2021 文档表示 Unity API 不是线程安全的,因此应该只使用 UnitySynchronizationContext(Sync 是同步) 内部的 async/await task。异步任务通常在调用时会创建对象,如果过度使用,可能会有性能问题。
注意
UnitySynchronizationContext 是 Unity同步上下文(环境),意思是同步去模拟异步?
在 Unity 中,UnitySynchronizationContext是同步上下文,它不是同步去模拟异步,而是用于协调异步操作在主线程上执行。非主线程可能无法安全访问 Unity API,所以要进行协调,将 Unity API 调度到主线程上,其他部分确实是异步多线程,此操作保证了异步操作与 Unity 主线程环境的兼容性,避免因线程导致的错误。
同步模拟异步更接近的是协程 Coroutine,Unity 的协程本质是单线程内的伪异步,它是有一个每帧检查器(定时器轮询),用于检测是否完成,它全部都运行在主线程上,会阻塞线程。但是使用起来会接近异步操作。
Unity 使用自定义 UnitySynchronizationContext 覆盖默认 SynchronizationContext,并默认在 Edit 和 Play 模式下运行主线程上的所有任务。
要使用异步任务,您必须使用 Task.Run API 手动创建和处理自己的线程,并使用默认的 SynchronizationContext 而不是 Unity 版本。
提示
Unity 在 2023 以上的版本已经可以直接使用 await 和 async 关键字。
如果需要使用多线程,应该使用 Unity 提供的 Job System 作业系统,作业系统安全地使用多个线程来并行执行作业。