1. 程式人生 > >IL2CPP 深入講解:垃圾回收器的整合

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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。