专题预览Unity 2022.1 测试版中的 IL2CPP 完全通用共享

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Dec 15, 2021|12 Min
专题预览Unity 2022.1 测试版中的 IL2CPP 完全通用共享
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

Full Generic Sharing可让你写出更为表意、测试更方便的代码。它不仅消除了仅在运行时出现的脚本错误,而且还让移动设备、主机等平台上的代码运行更可预测。详情请在下文了解。

它是什么?

泛型是 C# 的强大功能。它允许代码不限定类型就执行脚本行为。作为开发者,我们一直期望着List<string>的行为能与List<int>或支持任意类型的List<T>保持一致。

多年来,IL2CPP 一直在 T 是引用类型(字符串、对象等)的情况下使用泛型共享。这样做效果很好,因为 C# 中的引用类型总是用指针表示,所以 List<string> 的大小和实现将与 List<object> 的大小和实现相匹配。但如果T是一个在64位系统(指针为8个字节)上的int(4个字节)会发生什么?IL2CPP这时就必须为List<int>、List<double>、List<MyValueType>等生成特殊的代码。

这就是为什么在 Unity 2022.1 中,IL2CPP 已经生成了特殊代码,可以处理任何T、引用或值类型的 List<T>。这项技术被称为Full Generic Sharing(全泛型共享)。

它解决了什么问题?

虽然C#的泛型表达式可在即时编译(JIT)中顺利运行,但它难以在IL2CPP等超前编译(AOT)上执行。这时Full Generic Sharing就有用了。

让我们来看看Unity 手册中的一个泛型虚拟方法示例:

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);
}

这段代码展示了泛型方法的表达能力。换句话说,我们可以从任意用到IManager接口的类发送任何类型的数据(“message”)到任意使用了IReceiver接口的类。但在Unity 2021.2的IL2CPP下,这段看似简单的代码却无法运作。运行版日志在运行时会弹出以下错误:

ExecutionEngineException: Attempting to call method 'Test::OnMessage<Test+AnyEnum>' for which no ahead of time (AOT) code was generated. Consider increasing the --generic-virtual-method-iterations=1 argument

at Manager.SendMessage[T] (IReceiver target, T value) [0x00000] in <00000000000000000000000000000000>:0

at Test.Start () [0x00000] in <00000000000000000000000000000000>:0

让我们来仔细分析下该错误。

因为脚本在Start方法中使用了接口(IManager,也就是泛型“虚拟”的部分)来调用Send Message,IL2CPP在编译期间将无法检测运行时须调用的方法。

你可能想知道IL2CPP 怎么就想不明白呢?实际上,它能!IL2CPP可以在编译时搜索所有代码,并确定这次调用可能出现的地方。但这会耗费大量性能;这样做会不可避免地延长项目的构建时间,还有可能生成不会被调用的多余代码,让最终的游戏程序更为臃肿。

错误信息中提到的--generic-virtual-method-iterations argument设定的是IL2CPP执行代码搜索的时限。JIT编译器可以直接调用此类泛型虚拟方法,它可以在运行时“看到”目标方法并正确执行。在Unity 2022.1中,IL2CPP也学会了同样的技能。它现在可以生成一个可完全共享的、特殊的SendMessage,

用于处理各种T、引用或值类型,如果IL2CPP在编译时无法查询到目标方法,那么它将调用这个新版的SendMessage作替代。新版本下的C#代码将保留可读性、可在运行时执行,且编译速度也与JIT一样快。

多告诉我一些!

Full Generic Sharing技术非常实用,它可让AOT平台的代码执行起来更接近JIT平台的代码,有效减少运行时出现的意外。

事实证明,这些 ExecutionEngineException 错误在其他情况下也会出现。每当 IL2CPP 无法确定要运行哪些代码时,就会出现此错误。这种情况经常发生在序列化的数据上,部分新的序列化数据在反序列化后产生的类型经常无法为IL2CPP所推导。但在Unity 2022.1中,IL2CPP将不会再产生ExecutionEngineException,与之一同消失的还有这类难以改正的脚本错误。

当然,我们还要考虑到部分使用嵌套递归泛型的代码。IL2CPP在编译时可以无限地处理此类型,所以我们应对构建耗时进行限制。

当您的代码在运行时需要某些深嵌套类型时,IL2CPP 会产生以下错误:"IL2CPP 遇到了无法提前转换的托管类型。The type uses generic or array types, which are nested beyond the maximum depth that can be converted.”但现在Full Generic Sharing不会再让IL2CPP出错,此类错误也就随之消失了。

设想下你现在希望尽可能小地缩小项目体积,虽然C#提供有List<int>、List<double>和List<string>等多种泛型,但你也需要仔细平衡这么多种泛型的应用。

这时只用一个完全共享的List<T>泛型不是很好吗?去看看Player Settings下IL2CPP Code Generation选项“Faster (smaller) builds”,它使用了Full Generic Sharing来生成尽可能少的执行代码,极大地缩短了构建时间,并且,增量构建的速度也大大地加快了。即便项目用到了List<DateTime>(或其他什么类型),IL2CPP不必再专门生成或编译新代码。

下载beta版上手新功能

如果您想开始编写利用 IL2CPP Full Generic Sharing 的代码,只需从 Unity Hub 或我们的下载页面下载 Unity 2022.1 测试版即可注意,测试版并不适用于实际的项目生产。

不过,我们很想知道你的Unity 2022.1使用体验。请访问测试版论坛,给我们留下您的想法。任何对Full Generic Sharing或其他测试功能的反馈对我们来说都十分宝贵。为了感谢用户的参与,每位提交了独特、可复现Bug的用户将有机会参加我们的抽奖活动。详情请查看测试版发布博文