il2cpp_IL2CPP内部构件–垃圾收集器集成

il2cpp

这是IL2CPP Internals系列中的第七 。 在本文中,我们将探讨IL2CPP运行时如何与垃圾收集器集成。 具体来说,我们将看到托管代码中的GC根如何与本地垃圾收集器通信。 (This is the seventh post in the IL2CPP Internals series. In this post, we will explore a bit about how the IL2CPP runtime integrates with a garbage collector. Specifically, we’ll see how the GC roots in managed code are communicated to the native garbage collector.)

As with all of the posts in this series, this post deals with implementation details that can and likely will change in the future. In this post we will specifically look at some internal APIs used by the runtime code to communicate with the garbage collector. These APIs are not publicly supported, and you should not attempt to call them from any code in a real project. But, this is a post about internals, so let’s dig in.

与本系列的所有文章一样,本文章也讨论了将来可能会更改的实现细节。 在本文中,我们将专门研究运行时代码用于与垃圾收集器进行通信的一些内部API。 这些API不公开支持,您不应尝试从真实项目中的任何代码中调用它们。 但是,这是有关内部构件的文章,所以让我们深入研究。

垃圾收集 (Garbage collection)

I won’t discuss general garbage collection techniques in this post, as it is a wide and varied subject, with plenty of existing research and published information. To follow along, just think of a GC as an algorithm that develops a directed graph of object references. If an object Child is used by an object Parent (via a pointer in native code), then the graph looks like this:

我不会在本文中讨论一般的垃圾收集技术,因为它是一个广泛多样的主题,具有大量现有研究和已发布的信息。 接下来,可以将GC视为开发对象引用的有向图的算法。 如果父对象使用对象子对象(通过本机代码中的指针),则图形如下所示:

As the GC scans through the memory for a process, it looks for objects which don’t have parent. If it finds one, then it can reuse the memory for that object on something else.

当GC扫描内存以查找进程时,它将查找没有父对象的对象。 如果找到一个对象,则可以在其他对象上对该对象重新使用内存。

Of course, most object will have a parent of some sort, so the GC really needs to know which objects are the important parents. I like to think of these as the objects that are actually in use by your program. In GC terminology, these are called the “roots”. Here is an example of a parent without a root.

当然, 大多数对象都具有某种父对象,因此GC确实需要知道哪些对象是重要的父对象。 我喜欢将它们视为程序实际使用的对象。 在GC术语中,这些被称为“根”。 这是一个没有根的父母的例子。

In this case, Parent 2 does not have a root, so the GC can reuse the memory from Parent 2 and Child 2. Parent 1 and Child 1, however, do have a root, so the GC cannot reuse their memory. The program is still using them for something.

在这种情况下,父级2没有根,因此GC可以重用父级2和子级2的内存。但是,父级1和子级1确实具有根,因此GC无法复用其内存。 该程序仍在使用它们进行处理。

For .NET, there are three kinds of roots:

对于.NET,有三种根:

  • Local variables on the stack of any thread executing managed code

    执行托管代码的任何线程的堆栈上的局部变量
  • Static variables

    静态变量
  • GCHandle objects

    GCHandle对象

We’ll see how IL2CPP communicates with the garbage collector about all three of these kinds of roots.

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

设置 (The setup)

For this post, I’m using Unity 5.1.0p1 on OSX, and I’m building for the iOS platform. This will allow us to use Xcode to have a look at how IL2CPP interacts with the garbage collector. As with the other posts in this series, I’ll use an example project with a single script:

对于本文,我在OSX上使用Unity 5.1.0p1,并且正在为iOS平台构建。 这将使我们能够使用Xcode来了解IL2CPP如何与垃圾收集器进行交互。 与本系列的其他文章一样,我将使用一个脚本示例项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 ( ) ;
}
}

I have enabled the “Development Build” in the Build Settings dialog, and I set the “Run in Xcode as” option to a value of “Debug”. In the generated Xcode project, first search for the string “Start_m”. You should see the generated code for the Start method in the the HelloWorld class named HelloWorld_Start_m3.

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

将线程局部变量添加为根 (Adding thread local variables as roots)

Add a breakpoint in the HelloWorld_Start_m3 function on the line where Thread_Start_m9 is called. This method will create a new managed thread, so we expect that thread to be added to the GC as a root. We can see where this happens by exploring the libil2cpp header files that ship with Unity. In the Unity installation open the Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h file. This file has a number of methods prefixed with il2cpp_gc_ it serves as part of the API between the libil2cpp runtime and the garbage collector. Note that this is not a public API, so please don’t call these methods from any real project code. They are subject to change or removal without notice.

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

Let’s create a breakpoint in Xcode on the il2cpp_gc_register_thread function, using Debug > Breakpoints > Create Symbolic Breakpoint.

让我们使用Debug> Breakpoints> Create Symbolic Breakpoint在il2cpp_gc_register_thread函数上的Xcode中创建一个断点。

If you then run the project in Xcode, you’ll notice that the breakpoint is hit almost immediately. We can’t see the source code here, as it is built in the libil2cpp runtime static library, but we can see from the call stack that this thread is created in the InitializeScriptingBackend method, which executes when the player starts.

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

We will actually see this breakpoint hit a number of times, as the player creates each managed thread used internally. For now, you can disable this breakpoint in Xcode and allow the project to continue. We should hit the breakpoint we set earlier in the HelloWorld_Start_m3 method.

实际上,随着播放器创建内部使用的每个托管线程,我们将多次看到此断点。 现在,您可以在Xcode中禁用此断点并允许项目继续。 我们应该达到我们在HelloWorld_Start_m3方法中设置的断点。

Now we are just about to start the managed thread created by our script code, so enable the breakpoint on il2cpp_gc_register_thread again. When we hit that breakpoint, the first thread is waiting to join our created thread, but the call stack for the created thread shows that we are just starting it:

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

When a thread is registered with the garbage collector, the GC treats all objects on the local stack for that thread as roots. Let’s look at the generated code for the method we run on that thread (HelloWorld_AnotherThread_m4) :

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

1
2
3
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;
1
2
3
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 ;

We can see one local variable, L_0, which the GC must treat as a root. During the (short) lifetime of this thread, this instance of the AnyClass object and any other objects it references cannot be reused by the garbage collector. Variables defined on the stack are the most common kind of GC roots, as most objects in a program start off from a local variable in a method executing on a managed thread.

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

When a thread exits, the il2cpp_gc_unregister_thread function is called to tell the GC to stop treating the objects on the thread stack as roots. The GC can then work on reusing the memory for the AnyClass object represented in native code by L_0.

当线程退出时,将il2cpp_gc_unregister_thread函数,以告知GC停止将线程堆栈上的对象视为根对象。 GC能够然后重新使用存储器的工作AnyClass对象在由本机代码表示L_0

静态变量 (Static variables)

Some variables don’t live on thread call stacks though. These are static variables, and they also need to be handled as roots by the garbage collector.

但是,有些变量并不存在于线程调用堆栈中。 这些是静态变量,它们也需要由垃圾收集器作为根进行处理。

When IL2CPP lays out the native representation of a class, it groups all of the static fields together in a different C++ structure from the instance fields in the class. In Xcode, we can jump to the definition of the HelloWorld_t2 class:

当IL2CPP布置类的本机表示形式时,它将所有静态字段分组到与该类实例字段不同的C ++结构中。 在Xcode中,我们可以跳到HelloWorld_t2类的定义:

1
2
3
4
5
6
7
8
struct  HelloWorld_t2  : public MonoBehaviour_t3
{
};
struct HelloWorld_t2_StaticFields{
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2;
};
1
2
3
4
5
6
7
8
struct  HelloWorld _ t2  : public MonoBehaviour_t3
{
} ;
struct HelloWorld_t2_StaticFields {
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2 ;
} ;

Note that IL2CPP does not use the C++ static keyword, as it needs to be in control of the layout and allocation of the static fields to properly communicate with the GC. When a type is first used at runtime, the libil2cpp code will initialize the type. Part of this initialization involves allocating memory for the HelloWorld_t2_StaticFields structure. This memory is allocated with a special call into the GC: il2cpp_gc_alloc_fixed (also in the gc-internal.h file).

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

This call informs the garbage collector to treat the allocated memory as a root, and the GC dutifully does this for the lifetime of the process. It is possible to set a breakpoint on the il2cpp_gc_alloc_fixed function in Xcode, but it is called rather often (even for this simple project), so the breakpoint is not too useful.

此调用通知垃圾收集器将分配的内存视为根,GC会在进程的整个生命周期内尽职尽责。 可以在Xcode的il2cpp_gc_alloc_fixed函数上设置一个断点,但是它经常被调用(即使对于这个简单的项目),所以断点并不是太有用。

GCHandle对象 (GCHandle objects)

Suppose that you don’t want to use a static variable, but you still want a bit more control over when the garbage collector is allowed to reuse the memory for an object. This is usually helpful when you need to pass a pointer to a managed object from managed to native code. If the native code will take ownership of that object, we need to tell the garbage collector that the native code is now a root in its object graph. This works by using a special managed object called a GCHandle.

假设您不想使用静态变量,但是您仍然希望对何时允许垃圾收集器为对象重用内存进行更多控制。 当您需要从托管代码到本机代码传递指向托管对象的指针时,这通常很有用。 如果本机代码将拥有该对象的所有权,我们需要告诉垃圾回收器,本机代码现在是其对象图中的根。 这可以通过使用称为GCHandle的特殊托管对象来GCHandle

The creation of a GCHandle informs the runtime code that a given managed object should be treated as a root in the GC so that it and any objects it references will not be reused. In IL2CPP, we can see the low-level API to accomplish this in the Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h file. Again, this is not a public API, but it is fun to investigate. Let’s put a breakpoint on the GCHandle::New function. If we let the project continue then, we should see this call stack:

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

Notice that the generated code for our Start method is calling GCHandle_Alloc_m11, which eventually creates a GCHandle and informs the garbage collector that we have a new root object.

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

结论 (Conclusion)

We’ve looked at some internal API methods to see how the IL2CPP runtime interacts with the garbage collector, letting it know which objects are the roots it should preserve. Note that we have not talked at all about which garbage collector IL2CPP uses. It is currently using the Boehm-Demers-Weiser GC, but we have worked hard to isolate the garbage collector behind a clean interface. We currently have plans to research integration of the open-source CoreCLR garbage collector. We don’t have a firm ship date yet for this integration, but watch our public roadmap for updates.

我们已经研究了一些内部API方法,以了解IL2CPP运行时如何与垃圾收集器进行交互,让其知道哪些对象是应保留的根。 请注意,我们根本没有谈论过IL2CPP使用哪种垃圾收集器。 它目前正在使用Boehm-Demers-Weiser GC,但我们一直在努力将垃圾收集器隔离在干净的接口后面。 我们目前有计划研究开源CoreCLR垃圾收集器的集成。 我们还没有确切的出货日期,但是请关注我们的公开路线以获取更新。

As usual, we’ve just scratched the surface of the GC integration in IL2CPP. I encourage you to explore more about how IL2CPP and the GC interact. Please share your insights as well.

像往常一样,我们只是简单介绍了IL2CPP中GC集成的表面。 我鼓励您探索有关IL2CPP和GC如何相互作用的更多信息。 请也分享您的见解。

Next time, we will wrap up the IL2CPP internals series by looking at how we test the IL2CPP code.

下次,我们将通过查看如何测试IL2CPP代码来总结IL2CPP内部结构系列。

翻译自: https://blogs.unity3d.com/2015/07/09/il2cpp-internals-garbage-collector-integration/

il2cpp

相关文章:

  • 2021-12-11
  • 2021-11-29
  • 2021-10-23
猜你喜欢
  • 2021-08-04
  • 2021-05-19
  • 2021-11-26
  • 2022-12-23
  • 2021-08-03
  • 2021-07-22
  • 2022-01-06
相关资源
相似解决方案