原文链接:https://blog.unity.com/engine-platform/il2cpp-internals-generic-sharing-implementation
这是 IL2CPP Internals 系列的第五篇文章。在上一篇文章中,我们介绍了如何在为 IL2CPP 脚本后台生成的 C++ 代码中调用方法。在本篇文章中,我们将探讨这些方法是如何实现的。具体来说,我们将尝试更好地理解 IL2CPP 生成的代码中最重要的功能之一--泛型共享。泛型共享允许许多泛型方法共享一个共同的实现。这将大大减少 IL2CPP 脚本后台的可执行文件大小。 请注意,泛型共享并不是一个新想法,Mono 和 .Net 运行时也使用泛型共享。最初,IL2CPP 并不执行泛型共享。最近的改进使其更加强大和有益。由于 il2cpp.exe 生成的是 C++ 代码,因此我们可以看到方法实现的共享位置。
我们将探讨引用类型和值类型的泛型方法实现是如何共享(或不共享)的。 我们还将研究泛型参数约束如何影响泛型共享。
请记住,本系列讨论的所有内容都是实现细节。这里讨论的主题和代码将来很可能会发生变化。不过,我们喜欢在可能的情况下公开和讨论这样的细节!
想象一下,您正在用 C# 编写 List<T> 类的实现。该实现是否取决于 T 的类型?您能为 List<string> 和 List<object> 使用相同的 Add 方法实现吗?那么 List<DateTime> 呢?
事实上,泛型的强大之处就在于这些 C# 实现可以共享,泛型类 List<T> 可以适用于任何 T。但是,如果 List 从 C# 转换为可执行代码,如汇编代码(如 Mono 所做的)或 C++ 代码(如 IL2CPP 所做的),会发生什么情况呢?我们还能共享 Add 方法的实现吗?
是的,大多数情况下我们可以共享。我们将在这篇文章中发现,能否共享泛型方法的实现几乎完全取决于 T 类型的大小。如果 T 是任何引用类型(如字符串或对象),那么它总是指针的大小。如果 T 是值类型(如 int 或 DateTime),其大小可能会有所不同,情况就会变得复杂一些。可共享的方法实现越多,生成的可执行代码就越小。
马克-普罗普斯特(Mark Probst)是实现泛型共享的 Mono 开发人员,他有一系列关于 Mono 如何执行泛型共享的精彩文章。在此,我们不会深入讨论泛型共享。相反,我们将了解 IL2CPP 如何以及何时执行泛型共享。希望这些信息能帮助您更好地分析和理解项目的可执行文件大小。
目前,当 T 是如下类型时,IL2CPP 会共享通用类型 SomeGenericType<T> 的通用方法实现:
当 T 是值类型时,IL2CPP 不会共享泛型方法实现,因为每个值类型的大小不同(基于其字段的大小)。
实际上,这意味着添加 SomeGenericType<T> 的新用法(其中 T 是引用类型)对可执行文件大小的影响微乎其微。但是,如果 T 是值类型,可执行大小就会受到影响。这一行为对 Mono 和 IL2CPP 脚本后端都是一样的。如果您想了解更多,请继续阅读,是时候深入了解一些实现细节了!
我将在 Windows 上使用 Unity 5.0.2p1,并针对 WebGL 平台进行构建。我在构建设置中启用了 "“Development Player "选项,并将 "Enable Exceptions "选项设置为 "None"。本文章的脚本代码以一个驱动方法开始,该方法用于创建我们将要研究的泛型类型的实例:
public void DemonstrateGenericSharing() {
var usesAString = new GenericType<string>();
var usesAClass = new GenericType<AnyClass>();
var usesAValueType = new GenericType<DateTime>();
var interfaceConstrainedType = new InterfaceConstrainedGenericType<ExperimentWithInterface>();
}
接下来,我们定义该方法中使用的类型:
class GenericType<T> {
public T UsesGenericParameter(T value) {
return value;
}
public void DoesNotUseGenericParameter() {}
public U UsesDifferentGenericParameter<U>(U value) {
return value;
}
}
class AnyClass {}
interface AnswerFinderInterface {
int ComputeAnswer();
}
class ExperimentWithInterface : AnswerFinderInterface {
public int ComputeAnswer() {
return 42;
}
}
class InterfaceConstrainedGenericType<T> where T : AnswerFinderInterface {
public int FindTheAnswer(T experiment) {
return experiment.ComputeAnswer();
}
}