IL2CPP 内部:垃圾收集器集成

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 9, 2015|8 Min
IL2CPP 内部:垃圾收集器集成
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。
这是 IL2CPP Internals 系列的第七篇文章。在本篇文章中,我们将探讨一下 IL2CPP 运行时如何与垃圾收集器集成。具体来说,我们将看到托管代码中的 GC 根如何与本地垃圾回收器进行通信。

与本系列的所有文章一样,本文章涉及的实施细节在未来可能会发生变化。在本篇文章中,我们将具体介绍运行时代码用于与垃圾回收器通信的一些内部 API。这些 API 并不公开支持,您不应尝试在实际项目中的任何代码中调用它们。不过,这是一篇关于内部结构的文章,所以我们还是深入探讨一下吧。

垃圾收集

我不会在这篇文章中讨论一般的垃圾 Collections 技术,因为这是一个广泛而多样的主题,现有的研究和发布的信息很多。要理解这一点,只需将 GC 想象成一种开发对象引用有向图的算法。如果对象 "子对象 "被对象 "父对象 "使用(通过本地代码中的指针),那么图形看起来就像这样:

image03

当 GC 扫描内存中的进程时,它会寻找没有父进程的对象。如果找到了,就可以在其他地方重复使用该对象的内存。

当然,大多数对象会有某种父对象,因此 GC 确实需要知道哪些对象是重要的父对象。我喜欢把它们看作是程序实际使用的对象。在 GC 术语中,这些被称为 "根"。下面是一个没有根的父代的例子。

image02

在这种情况下,父 2 没有根,因此 GC 可以重复使用父 2 和子 2 的内存。不过,父 1 和子 1 确实有一个根,所以 GC 不能重复使用它们的内存。该计划仍在利用它们做一些事情。

对于 .NET,有三种根:

- 任何执行托管代码的线程堆栈上的 Local 变量

- 静态变量

- GCHandle对象

我们将看到 IL2CPP 如何就这三种根与垃圾回收器进行通信。

设置

在这篇文章中,我使用的是 OSX 上的 Unity 5.1.0p1,并且是为 iOS 平台构建的。这将允许我们使用 Xcode 来查看 IL2CPP 如何与垃圾回收器交互。与本系列的其他文章一样,我将使用一个带有单一脚本的示例项目:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;

public class AnyClass {}

public class HelloWorld : MonoBehaviour {
private static AnyClass staticAnyClass = new AnyClass();
void Start () {
var thread = new Thread(AnotherThread);
thread.Start();
thread.Join();
var anyClassForGCHandle = new AnyClass();
var gcHandle = GCHandle.Alloc(anyClassForGCHandle);
}

private static void AnotherThread() {
var anyClassLocal = new AnyClass();
}
}

我在 "构建设置 "对话框中启用了 "开发构建",并将 "在 Xcode 中作为......运行 "选项设置为 "调试"。在生成的 Xcode 项目中,首先搜索字符串 "Start_m"。您应该会看到 HelloWorld 类中名为 HelloWorld_Start_m3 的 Start 方法的生成代码。

添加线程 Local 变量作为根变量

在 HelloWorld_Start_m3 函数中调用 Thread_Start_m9 的一行添加一个断点。该方法将创建一个新的托管线程,因此我们希望该线程能作为根线程添加到 GC 中。我们可以通过查看 Unity 随附的 libil2cpp 头文件来了解发生这种情况的原因。在 Unity 安装程序中打开 Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h 文件。该文件包含许多以 il2cpp_gc_ 为前缀的方法,是 libil2cpp 运行时与垃圾回收器之间的 API 的一部分。请注意,这不是一个公共 API,因此请不要从任何实际项目代码中调用这些方法。如有更改或删除,恕不另行通知。

让我们在 Xcode 中使用 "调试">"断点">"创建符号断点 "在 il2cpp_gc_register_thread 函数上创建一个断点。

image04

如果您在 Xcode 中运行该项目,您会发现断点几乎立即被击中。我们无法看到源代码,因为它是在 libil2cpp 运行时静态库中构建的,但我们可以从调用堆栈中看到,该线程是在 InitializeScriptingBackend 方法中创建的,该方法在播放器启动时执行。

image01

实际上,当播放器创建内部使用的每个托管线程时,我们会多次看到这个断点被击中。目前,您可以在 Xcode 中禁用此断点,让项目继续进行。我们应该点击之前在 HelloWorld_Start_m3 方法中设置的断点。

现在我们正要启动脚本代码创建的托管线程,因此再次启用 il2cpp_gc_register_thread 上的断点。当我们点击该断点时,第一个线程正等待加入我们创建的线程,但创建线程的调用堆栈显示我们刚刚开始启动它:

image05

当线程向垃圾回收器注册时,GC 会将该线程本地堆栈上的所有对象视为根对象。让我们看看在该线程上运行的方法(HelloWorld_AnotherThread_m4)的生成代码:

AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var);
AnyClass__ctor_m0(L_0, /*hidden argument*/NULL);
V_0 = L_0;

我们可以看到一个本地变量 L_0,GC 必须将其视为根。在该线程的(短暂)生命周期内,垃圾回收器不能重复使用 AnyClass 对象的该实例及其引用的任何其他对象。栈中定义的变量是最常见的 GC 根,因为程序中的大多数对象都是从托管线程上执行的方法中的 Localization 变量开始的。

当线程退出时,会调用 il2cpp_gc_unregister_thread 函数来告诉 GC 停止将线程栈上的对象视为根。然后,GC 就可以为本地代码中由 L_0 表示的 AnyClass 对象重新使用内存。

静态变量

不过,有些变量并不存在于线程调用栈中。这些都是静态变量,垃圾回收器也需要将它们作为根变量处理。

当 IL2CPP 对一个类进行本地表示时,它会将所有静态字段组合在一个不同于类中实例字段的 C++ 结构中。在 Xcode 中,我们可以跳转到 HelloWorld_t2 类的定义:

struct  HelloWorld_t2  : public MonoBehaviour_t3
{
};

struct HelloWorld_t2_StaticFields{
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2;
};

请注意,IL2CPP 不使用 C++ 的 static 关键字,因为它需要控制静态字段的布局和分配,以便与 GC 正确通信。运行时首次使用类型时,libil2cpp 代码将初始化该类型。初始化工作的一部分是为 HelloWorld_t2_StaticFields 结构分配内存。该内存是通过对 GC 的特殊调用分配的:il2cpp_gc_alloc_fixed(也在 gc-internal.h 文件中)。

该调用会通知垃圾回收器将已分配的内存作为根内存处理,GC 会在进程的生命周期内尽职尽责地完成这项工作。在 Xcode 中可以对 il2cpp_gc_alloc_fixed 函数设置断点,但它被频繁调用(即使对于这个简单的项目),因此断点的作用不大。

GCHandle 对象

假设你不想使用静态变量,但仍想对垃圾回收器何时允许重用对象的内存进行更多控制。当需要从托管代码向本地代码传递指向托管对象的指针时,这通常很有用。如果本地代码将获得该对象的所有权,我们就需要告诉垃圾回收器,本地代码现在是其对象图中的根。这是通过使用一个名为GCHandle 的特殊托管对象来实现的。

GCHandle 的创建会通知运行时代码,给定的托管对象在 GC 中应被视为根对象,这样它及其引用的任何对象都不会被重复使用。在 IL2CPP 中,我们可以在 Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h 文件中看到实现这一点的底层 API。同样,这也不是一个公开的 API,但研究起来很有趣。让我们在 GCHandle::New 函数上设置一个断点。如果我们让项目继续运行,就会看到这样的调用堆栈:

image00

请注意,Start 方法的生成代码正在调用 GCHandle_Alloc_m11,最终会创建一个 GCHandle,并通知垃圾回收器我们有了一个新的根对象。

结论

我们查看了一些内部 API 方法,了解 IL2CPP 运行时如何与垃圾回收器互动,让它知道哪些对象是应该保留的根。请注意,我们完全没有讨论 IL2CPP 使用哪种垃圾回收器。它目前使用Boehm-Demers-WeiserGC,但我们努力将垃圾回收器隔离在一个干净的界面后面。我们目前计划研究如何集成开源CoreCLR垃圾收集器。我们尚未确定该集成的发货日期,但请关注我们的公开路线图更新。

与往常一样,我们对 IL2CPP 中的 GC 集成还只是浅尝辄止。我鼓励您进一步了解 IL2CPP 和 GC 如何相互作用。也请分享您的见解。

下一次,我们将介绍如何测试 IL2CPP 代码,从而结束 IL2CPP 内部结构系列。