Unity IL2CPP 编码限制
参考
- unity documentation-Scripting restrictions 2018.4
- unity documentations-Scripting restrictions 2022.3
内容相差不大,2022.3在2018.4的基础上做了一些补充,并去除了一个关于泛型虚方法例子。
Ahead-of-time编译(AOT)
一些平台不允许运行时的代码生成(运行时开辟内存用于存放可执行的机器指令)。因此,一切依赖于just-in-time(JIT)的托管代码(managed code)在这些平台上都无法运行。为此,你需要把所有的托管代码都进行AOT编译。在一般情况下,这个限制不会带来什么影响。但在AOT编译的平台上,也存在着一些需要额外注意的要点。
自省/反射 Reflection
在AOT平台,反射仍然是支持的。但如果编译器不能够推断出一段代码会被反射执行,那么这段代码在运行时可能就不会存在,更多内容可以参考Managed Code Stripping一文。
System.Reflection.Emit
在AOT的平台上无法实现任何在System.Reflection.Emit
命名空间下的方法。其余System.Reflection
命名空间下的内容都可以正常执行,只要如上所说,编译器能够推断出这段代码会被通过反射执行。
序列化
AOT平台如果在序列化或者反序列化的过程使用了反射,那么有可能会遇到问题。如果一个类型或者方法只在序列化或者反序列化的过程中通过反射被获取到,那么AOT编译器就无法推断出需要为这个类型或者方法生成相应的代码。
泛型类型与方法
对于泛型类型和方法,编译器必须判断出那些泛型实例被使用了,因为不同的泛型实例可能需要不同的生成代码。举例来说,List<int>
和List<double>
所需要的的生成代码就是不同的。不过,由于IL2CPP会在引用类型间共用代码,所以List<object>
和List<string>
就会共用一份生成代码。
在下列的这些情况中,IL2CPP很可能无法在编译期间生成所需的代码:
运行时创建泛型实例:
Activator.CreateInstance(typeof(SomeGenericType<>).MakeGenericType(someType));
通过泛型实例调用一个静态方法:
typeof(SomeGenericType<>).MakeGenericType(someType)).GetMethod(“AMethod”).Invoke(null, null);
调用一个静态的泛型方法:
typeof(SomeType).GetMethod(“GenericMethod”).MakeGenericMethod(someType).Invoke(null, null);
一些在编译期间无法被推断出的对泛型虚方法的调用,在2018.4的文档中给出了这样一个例子
using UnityEngine; using System; public class AOTProblemExample : MonoBehaviour, IReceiver { public enum AnyEnum { Zero, One, } void Start() { // Subtle trigger: The type of manager *must* be // IManager, not Manager, to trigger the AOT problem. 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); }
你会得到这样的报错:
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
涉及高度嵌套的泛型值类型方法调用,如
Struct<Struct<Struct<...<Struct<int>>>>
IL2CPP为了支持上述这些情况,生成了能够支持任何类型参数的泛型代码。不过这份代码执行起来会更慢,因为它不能就类型的大小或者值类型、引用类型的差异来做出假设。如果你需要一份执行效率更高的泛型生成代码,你需要这样做:
如果泛型参数永远是引用类型,为它加上
where:class
的限制。这样IL2CPP就会生成一个使用引用类型的fallback方法,它不会带来额外的性能损耗。如果泛型参数永远是值类型,加上
where:struct
限制,这将启用一些优化策略,但因为值类型的大小可能不同,生成的代码执行起来仍然会更慢一些。新增一个
UsedOnlyForAOTCodeGeneration
方法,并且为你希望IL2CPP生成的泛型类型、泛型方法加上引用。这个方法不需要被调用,也不应该被调用。下述的例子保证了GenericType<MyStruct>
会被生成。public void UsedOnlyForAOTCodeGeneration() { // Ensure that IL2CPP will create code for MyGenericStruct // using MyStruct as an argument. new GenericType<MyStruct>(); // Ensure that IL2CPP will create code for SomeType.GenericMethod // using MyStruct as an argument. new SomeType().GenericMethod<MyStruct>(); public void OnMessage<T>(T value) { Debug.LogFormat("Message value: {0}", value); } // Include an exception so we can be sure to know if this // method is ever called. throw new InvalidOperationException( "This method is used for AOT code generation only. " + "Do not call it at runtime."); }
需要注意的是,当“Faster(smaller) builds”设置项启用时,只有一份完全共用的泛型代码会被生成并编译。这回较少生成的方法数量,减少编译时长与包体大小,但会带来运行时的性能开销。
从原生层(native code)调用托管层(managed)方法
在AOT平台上,那些需要被转译(marshaled?不确定如何翻译比较准确)为C方法指针以便于从原生层调用的托管层方法,有一些限制:
托管方法必须是一个静态方法
托管方法必须要有
[MonoPInvokeCallback]
Attribute如果这个托管方法是泛型的,那么便需要使用
[MonoPInvokeCallback(Type)]
重载来声明哪些具体的类型需要被使用。Type必须要是带有正确泛型参数数量的泛型实例。对一个方法使用复数个[MonoPInvokeCallback]
是可能的,如下例:// Generates reverse P/Invoke wrappers for NameOf<long> and NameOf<int> // Note that the types are only used to indicate the generic arguments. [MonoPInvokeCallback(typeof(Action<long>))] [MonoPInvokeCallback(typeof(Action<int>))] private static string NameOfT<T>(T item) { return typeof(T).Name; }
线程不支持
一些平台不支持对线程的使用,所以一切使用了System.Threading
命名空间的托管层代码都会在运行时出错。同时,一些.NET类库隐式地依赖了线程。一个依赖线程支持的常见例子就是System.Timers.Timer
类。
异常过滤器
IL2CPP支持异常过滤器,不过异常过滤语句和catch块的次序可能会存在不同,因为IL2CPP使用C++的异常来实现托管层异常。一般情况下这不会被注意到,除非有过滤器阻止了对字段的写操作。
其他
- IL2CPP不支持运行时对
MarshalAs
以及FieldOffset
Attributes 的反射 - IL2CPP不支持C#的
dynamic
关键字,因为它需要JIT编译 - IL2CPP不支持
Marshal.Prelink
或Marshal.PrelinkAll
API方法 - IL2CPP不支持
System.Diagnostics.Process
API方法