Unity 致力于在支持的所有平台之间提供通用的脚本 API 和体验。但是,有些平台存在固有的限制。为帮助您了解这些限制并支持跨平台代码,下表描述了每个平台和脚本后端适用的限制:
平台(脚本后端) | 提前编译 | 无线程 | .NET Core 类库子集 | |
---|---|---|---|---|
Android (IL2CPP) | ✔ | |||
Android (Mono) | ||||
iOS (IL2CPP) | ✔ | |||
PlayStation 4 (IL2CPP) | ✔ | |||
PlayStation Vita (IL2CPP) | ✔ | |||
独立平台 (IL2CPP) | ✔ | |||
独立平台 (Mono) | ||||
Switch (IL2CPP) | ✔ | |||
通用 Windows 平台 (IL2CPP) | ✔ | |||
通用 Windows 平台 (.NET) | ✔ | |||
WebGL (IL2CPP) | ✔ | ✔ | ||
WiiU (Mono) | ||||
XBox One (IL2CPP) | ✔ |
有些平台不允许生成运行时代码。因此,任何依赖于在目标设备上即时 (JIT) 编译的托管代码都将失败。相反,需要提前 (AOT) 编译所有托管代码。通常,这种区别并不重要,但在少数特定情况下,AOT 平台需要额外注意。
AOT 平台无法实现 System.Reflection.Emit
命名空间中的任何方法。System.Reflection
的其余部分是可接受的,只要编译器可以推断通过反射使用的代码需要在运行时存在。
AOT 平台可能会由于使用了反射而遇到序列化和反序列化问题。如果仅通过反射将某个类型或方法作为序列化或反序列化的一部分使用,则 AOT 编译器无法检测到需要为该类型或方法生成代码。
通用方法要求编译器做一些额外的工作,将开发人员编写的代码扩展到设备上执行的代码。例如,对于具有 int
或 double
类型的 List
,需要不同代码。虚拟方法在运行时而不是编译时确定行为,存在虚拟方法时,编译器可在不完全明显的地方轻松地要求从源代码生成运行时代码。
以下代码示例在 JIT 平台上完全按预期工作(向控制台输出一次“Message value: Zero”):
using UnityEngine;
using System;
public class AOTProblemExample : MonoBehaviour, IReceiver
{
public enum AnyEnum
{
Zero,
One,
}
void Start()
{
// 微妙的触发器:管理器的类型*必须*是
// IManager(而不是 Manager)才能触发 AOT 问题。
IManager manager = new Manager();
manager.SendMessage(this, AnyEnum.Zero);
}
public void OnMessage<T>(T value)
{
Debug.LogFormat("Message value: {0}", value);
}
}
public class Manager : IManager
{
public void SendMessage<T>(IReceiver target, T value) {
target.OnMessage(value);
}
}
public interface IReceiver
{
void OnMessage<T>(T value);
}
public interface IManager
{
void SendMessage<T>(IReceiver target, T value);
}
但是,使用 IL2CPP 脚本后端在 AOT 平台上执行此代码时,发生以下异常:
ExecutionEngineException: Attempting to call method 'AOTProblemExample::OnMessage<AOTProblemExample+AnyEnum>' for which no ahead of time (AOT) code was generated.
at Manager.SendMessage[T] (IReceiver target, .T value) [0x00000] in <filename unknown>:0
at AOTProblemExample.Start () [0x00000] in <filename unknown>:0
同样,Mono 脚本后端提供以下类似的异常:
ExecutionEngineException: Attempting to JIT compile method 'Manager:SendMessage<AOTProblemExample/AnyEnum> (IReceiver,AOTProblemExample/AnyEnum)' while running with --aot-only.
at AOTProblemExample.Start () [0x00000] in <filename unknown>:0
AOT 编译器不会意识到自己应该为 T
为 AnyEnum
的泛型方法 OnMessage
生成代码,所以它会继续往下,跳过该方法。调用该方法时,运行时无法找到要执行的正确代码,因此返回此错误消息。
要解决像这样的 AOT 问题,可以强制编译器生成适当的代码。可以向 AOTProblemExample
类添加以下示例方法:
public void UsedOnlyForAOTCodeGeneration()
{
// IL2CPP 只需要这一行。
OnMessage(AnyEnum.Zero);
//Mono 也需要这一行。请注意,我们
// 直接在 Manager 而不是 IManager 接口上调用。
new Manager().SendMessage(null, AnyEnum.Zero);
//包含一个异常,这样我们就可以确定是否调用过该方法。
throw new InvalidOperationException("This method is used for AOT code generation only.Do not call it at runtime.");
}
当编译器遇到 T
为 AnyEnum
的 OnMessage
的显式调用时,它会生成运行时执行的正确代码。不需要调用 UsedOnlyForAOTCodeGeneration
方法;该方法的存在只是为了让编译器看到而已。
需要编组到 C 函数指针以便可以从原生代码调用的托管方法会在 AOT 平台上有一些重构:
[MonoPInvokeCallback]
属性有些平台不支持使用线程,因此任何使用 System.Threading
命名空间的托管代码都将在运行时失败。此外,.NET 类库的某些部分存在对线程的隐式依赖。一个常用的例子是 System.Timers.Timer
类,它依赖于对线程的支持。
IL2CPP 不支持 C# 异常过滤器。应该将依赖于异常过滤器的代码修改为正确的 catch
块。
IL2CPP 不支持 System.TypedReference
类型和 __makeref
C# 关键字。