IL2CPP 深入讲解:垃圾回收器的集成

系列的第七篇博文了,在本篇文章中,我们将探讨IL2CPP运行时如何于垃圾收集器协同工作。特别地,我们将会看到在托管代码中起作用的GC是如何和原生代码的GC进行交流的。

整个系列都在强调,本篇也不例外:文中所描述的技术细节很有可能在未来会发生变化。在这篇文中中,我们还将会看到内部调用的API函数,它们被用来和垃圾收集器进行通讯。这些API没有被公开,因此你也不应该在正式的项目中使用这些函数。

垃圾收集

我在这里不会对垃圾回收的基本概念进行讨论,这是一个宽泛和多样化的主题,你可以在很多地方找到有关垃圾回收基本概念的文章。在这里,你可以把GC想象成一个描述程序对象之间互相引用的算法。如果在程序中一个子对象被其父对象引用(在原生代码中使用的是指针),那他们的关系图如下:

当GC对一个进程的内存进行扫描,它会尝试寻找没有父亲的对象。如果找到,就对他们进行回收以便释放内存给其他需求使用。
当然,其实大部分的对象都有父对象,因此GC需要确切的知道哪些对象是重要的父对象,这些对象是你的程序中真正正在使用的。用GC的术语来说,这些对象叫做“根”。下面的例子展示了两种不同的情况:

在上面的图片中,Parent2没有根对象,因此GC可以释放Parent2和其孩子Child2的内存以便重新使用。但是Parent1和Child1的情况不同,Parent1有根对象,因此GC不能回收他们,因为他们还在被程序使用。

对于.NET来说,有三类根对象:

    在托管代码线程栈上的局部变量

    静态变量

    GCHandle 对象

我们就来谈谈IL2CPP如何就这三类根对象和垃圾收集器进行通讯。

项目设置

以下的例子我将会使用OSX上的的Unity 5.1.0p1版本,我将目标平台设置成iOS,这样可以让我们通过Xcode来观察IL2CPP和GC的交流的情况。

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

在构建设置中打开“Development Build”,并且在“Run in Xcode”中将值设置成“Debug”。在生成的Xcode项目中,首先搜索字符串“Start_m”。你应该能找到为HelloWorld类中Start函数生成的HelloWorld_Start_m3原生代码函数。
将线程中的局部变量添加成为“根”对象

在HelloWorld_Start_m3中的Thread_Start_m9处添加一个断点。这个函数会创建一个新的托管线程,因此这个线程会当成“根”对象被添加到GC中。我们能在随Unity一起发布的libil2cpp的头文件中一探究竟。在Unity的安装目录中,打开Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h文件。这个文件中有一些有着“il2cpp_gc_”前缀的函数。他们就是libil2cpp运行时和垃圾收集器之间的桥梁。请注意这些都不是公开的API,所以请不要在实际的项目中使用他们,Unity会在没有通知的情况下变化接口或者直接删除他们。

让我们使用Debug > Breakpoints > Create Symbolic Breakpoint菜单命令在il2cpp_gc_register_thread函数中设置一个断点:

如果你在Xcode中运行项目,你会注意到这个断点几乎是立刻被触发了。在这里我们不能看到源码,源码在libil2cpp的运行时的静态库中,但是我们在调用栈中可以看到这个线程是在程序一开始就执行的InitializeScriptingBackend函数中被创建的。

事实上我们会发现每当托管线程被创建的时候,这个断点都会被触发。就目前而言,你可以让这个断点暂时无效从而让代码能够继续运行。接下来我们会触发最一开始在HelloWorld_Start_m3函数中设置的断点。
现在到了我们的代码创建线程的地方了,所以再次让在il2cpp_gc_register_thread函数中的第二个断点有效。当断点被触发的时候,第一个线程正在等待加入我们创建的线程,而在新创建线程的栈中显示线程已经开始运行了:

当一个线程被注册到垃圾回收器中,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回收另作他用。绝大部分在栈上的变量对于GC来说都是“根”对象,因为这些变量所在的函数都是在一个线程中被执行的。

当线程退出的时候,il2cpp_gc_unregister_thread函数被调用,用来告知GC这些对象已经不是“根”对象了。因此GC可以通过回收L_0来释放AnyClass所占用的内存空间。

静态变量

有些变量不在线程调用栈上。这些静态变量也需要被当成“根”对象处理。

当IL2CPP生成原生代码时,它会把所有静态成员集中到另外一个C++的结构中。在例子中的代码中,我们可以找到 HelloWorld_t2类的定义:

 struct  HelloWorld_t2  : public MonoBehaviour_t3
 {
 };

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

请注意这里IL2CPP并没有使用C++中的static关键字,因为IL2CPP需要控制这些静态成员的创建以便能和GC进行通讯。当这些类型第一次被使用到的时候,libil2cpp中的代码会对其进行初始化。初始化中包括了对HelloWorld_t2_StaticFields结构的内存分配。这些内存分配是调用了特殊的GC内部函数il2cpp_gc_alloc_fixed(这个函数也位于gc-internal.h头文件中)实现的。

这个函数会通知GC对这些分配的内存全部当成“根”对象处理。因此GC会很负责的在整个进程中保持这些对象。在il2cpp_gc_alloc_fixed中设置断点也是可行的,不过因为这个函数很少被调用,因此这个断点不是太有用。

GCHandle对象

假设你不想使用静态变量,但是你又想当GC回收他们的时候对变量有更多一些的控制权。特别的,当你将托管代码中的一个对象传递给原生代码,而原生代码又要保持这个对象的时候,我们必须告诉GC这些对象是“根”对象,不能被回收。这个是通过一个特殊的GCHandle对象来实现的。

GCHandle的创建告诉运行时代码这些对象应当被当成“根”对象处理,此对象以及其引用到的对象都不能回收重用。在IL2CPP中,我们能在Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h文件中看到相关的API函数。

再次强调,这些不是公开的API函数,不过深入调查一下还是蛮有趣的。让我们放一个断点在GCHandle::New函数中。如果我们让项目继续运行,就能触发这个断点:

从栈上我们可以看出实际的调用函数是GCHandle_Alloc_m11,在这个函数里创建了GCHandle对象并且通知GC其是“根”对象。

总结

我们检视一些内部的API函数来搞清楚IL2CPP运行时是如何和GC交互的:通过“根”对象的概念让GC知道哪些对象可以被回收,而哪些不行。大家或许注意到了我们在这里并没有讨论IL2CPP用的是哪种垃圾收集器。目前正在使用的是Boehm-Demers-Weiser垃圾收集器,同时我们也有计划去研究另一个开源的CoreCLR垃圾收集器。对于新的垃圾收集器集成,我们并没有一个具体的时间表,大家可以关注我们roadmap的更新。

和往常一样,本文只是讲述了IL2CPP中有关GC的一些皮毛而已,我鼓励大家自己继续探索研究IL2CPP是如何和GC交互的。

下次,通过讲述我们如何对IL2CPP代码进行测试来对深入讲解系列做一个总结。



作者:IndieACE
链接:https://www.jianshu.com/p/59d3ab1135b3
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自blog.csdn.net/chqj_163/article/details/83627129