Version: 2021.1
托管堆栈跟踪与 IL2CPP
托管代码剥离

脚本限制

Unity 在支持的所有平台之间提供通用的脚本 API 和体验。但是,有些平台存在固有的限制。为帮助您了解这些限制,下表描述了每个平台和脚本后端适用的限制:

.NET 4.x 等效脚本运行时

平台(脚本后端) 提前编译 无线程 .NET Core 类库子集
Android (IL2CPP)
Android (Mono)
iOS (IL2CPP)
独立平台 (IL2CPP)
独立平台 (Mono)
通用 Windows 平台 (IL2CPP)
通用 Windows 平台 (.NET)
WebGL (IL2CPP)

提前编译

有些平台不允许生成运行时代码。因此,任何依赖于在目标设备上即时 (JIT) 编译的托管代码都将失败。相反,必须提前 (AOT) 编译所有托管代码。通常,这种区别并不重要,但在少数特定情况下,AOT 平台需要额外注意。

System.Reflection.Emit

AOT 平台无法实现 System.Reflection.Emit 命名空间中的任何方法。System.Reflection 的其余部分是可接受的,只要编译器可以推断通过反射使用的代码需要在运行时存在。

序列化

AOT 平台可能会由于使用了反射而遇到序列化和反序列化问题。如果仅通过反射将某个类型或方法作为序列化或反序列化的一部分使用,则 AOT 编译器无法检测到需要为该类型或方法生成代码。

通用虚拟方法

如果使用泛型方法,编译器必须做一些额外的工作,才能将您编写的代码扩展到设备上执行的代码。例如,对于具有 intdouble 类型的 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 编译器不会意识到自己应该为 TAnyEnum 的泛型方法 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.");
}

当编译器遇到 TAnyEnumOnMessage 的显式调用时,它会生成运行时执行的正确代码。不需要调用 UsedOnlyForAOTCodeGeneration 方法;该方法的存在只是为了让编译器看到而已。

从原生代码调用托管方法

需要编组到 C 函数指针以便可以从原生代码调用的托管方法会在 AOT 平台上有一些限制:

  • 托管方法必须是静态方法
  • 托管方法必须具有 [MonoPInvokeCallback] 属性

无线程

有些平台不支持使用线程,因此任何使用 System.Threading 命名空间的托管代码都将在运行时失败。此外,.NET 类库的某些部分存在对线程的隐式依赖。一个常用的例子是 System.Timers.Timer 类,它依赖于对线程的支持。

异常过滤器

IL2CPP 不支持 C# 异常过滤器。应该将依赖于异常过滤器的代码修改为正确的 catch 块。

TypedReference

IL2CPP 不支持 System.TypedReference 类型和 __makeref C# 关键字。

MarshalAs 和 FieldOffset 属性

IL2CPP 不支持在运行时反射 MarhsalAsFieldOffset 属性。它在编译时支持这些属性。应正确使用它们以进行正确的平台调用编组

动态关键字

IL2CPP 不支持 C# dynamic 关键字。此关键字需要 JIT 编译,而 IL2CPP 无法实现。

托管堆栈跟踪与 IL2CPP
托管代码剥离