[Unity] 關於 Component 的 GC 測試,出現了大問題! – Testing GC of Component in Unity

不久前才測試完了 Delegate 的 GC,雖然只是驗證了一個可以預期的結果。不過才過幾小時,我就想到類似的議題在 Unity Component 上,是否會因為 Component 特性而產生不太一樣的結果?

雖然本是要測試 Delegate,不過我同時也想驗證一下之前就發現的一個 Component 特性:自行移除相關參照 (Reference)

結果竟然在測試過程中有了額外發現,間接造成 Delegate 的測試無法進行下去… 所以文章便直接停止在 Unity Component 的 GC 測試。

測試用 Component

為了驗證 GC,必須先實作出一個測試用的單元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ComponentA : MonoBehaviour {

public static int Count = 0;

public ComponentA() {
Count += 1;
}

~ComponentA() {
Count -= 1;
}

public void FunctionA () {
Debug.Log ("FunctionA in ComponentA");
}
}

在 C# 的 GC 機制下,類別的解構子 (Destructor) 會在被 GC 的瞬間執行,所以透過解構子的可以確定 GC 的執行狀況,這點在一般情況下是完全說得通的。

但因為 Component 在 Unity 之中有著另外一套獨有的生命週期,所以實際開發之中是絕對不建議在 Component 之中實作建構子 (Constructor) 以及解構子 (Destructor) 的,這邊是測試需要才特別如此應用。

測試用腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ReferenceTest : MonoBehaviour {

private ComponentA refernceA;
private ComponentA refernceB;

private void Start () {
CheckReference ();

refernceA = refernceB = this.gameObject.AddComponent<ComponentA> ();
CheckReference ();

Destroy (refernceA);
CheckReference ();
}

public void CheckReference () {
Resources.UnloadUnusedAssets ();
System.GC.Collect ();
Debug.Log ("ComponentA.Count = " + ComponentA.Count);

if (refernceA == null) {
Debug.Log ("refernceA is null");
} else {
Debug.Log ("refernceA is " + refernceA.GetType ());
}
if (refernceB == null) {
Debug.Log ("refernceB is null");
} else {
Debug.Log ("refernceB is " + refernceB.GetType ());
}
}
}

測試結果

componentreference

這邊測試結果的圖片,紅線以上的部分是 ReferenceTest.Start() 所執行的部分,紅線以下是另外使用按鈕呼叫 ReferenceTest.CheckReference() 的結果。

在前 3 行中,ComponentA 還沒有實體,所以 ComponentA.Count 為 0。

在 4 到 6 行間,已經執行了 AddComponent 產生實體,同時參照到 referenceA、referenceB 兩個變數上進行儲存,此時 ComponentA.Count 為 1。

在 7 到 9 行間,已經執行了 Destroy 的動作來移除 ComponentA,而 refernceA、referenceB 兩個變數的參照依舊,並沒有被設為 null,此時 ComponentA.Count 為 1。

到此為止,一切的運作都跟一般類別沒甚麼不同,Destroy() 這個方法會將場景中的 ComponentA 移除,但並沒有自動將 refernceA 設為 null,referenceB 沒有參與動作,所以也保持著參照而沒有設為 null。所以為了正確執行 GC,似乎得手動將 refernceA、referenceB 這兩個變數手動設為 null?

… (請停頓消化一下)

不,我們不另外進行手動設值為 null,在等待幾個 frame 之後再次執行 CheckReference() 檢查 refernceA、referenceB 這兩個變數。

明明甚麼事都沒做,refernceA、referenceB 兩個變數自己就變成 null 了!我所能提出來的假設,就是 Unity Component (MonoBehaviour) 的生命週期中,運作完 OnDestroy 等事件之後,會在生命週期的最後透過 Unity 本身的某種機制將所有關聯的參照都自動設為 null,來避免開發者無意間使用了已經失效的 Component。

這不是又神奇又方便嗎!以上便是我在無意間發現的一個 Component 特性。

… (請再停頓消化一下)

等等,此時 ComponentA.Count 依舊為 1?說好的沒有參照就會被 GC 呢?

大問題

在上一段落的最後,又發現了 Unity 似乎有著不明的機制干擾著 .Net 進行 GC 的動作, 因此 Delegate 的 GC 測試無法進行下去。

為了瞭解這問題的細節,我在網路上尋找了相關問題的討論,但是暫時沒有確切結果。

  • 參考連接 1 提到,Object.Destory 到 Despose 之間 Unity 做了一些神奇的事情。
  • 參考連結 2 提到,只在 Editor 環境下,Unity 會用設值為 null 來代替實際 Dispose (Why did you do this?),而在輸出專案後,便會正常 GC (在測試驗證前我持保留態度)。
  • 參考連結 3 提到了 Object.DestroyImmediate() 這個方法,經過測試確認,它可以在執行後立刻自行移除相關參照,但不建議使用,且同樣不會引起 GC。

未來有機會我想解決兩個疑問:

  • 自行移除相關參照的特性能否在輸出專案之後繼續成立,能否實際應用於開發上?
  • Unity 的某個機制會影響 Component 的 GC,是否輸出專案之後就真的沒有問題?還是依然有特定的地方要注意?

參考連結

  1. http://answers.unity3d.com/questions/584324/is-a-unity-object-really-destroyed-if-its-destruct.html
  2. https://forum.unity3d.com/threads/how-does-unity-implement-nulling-references.38121/
  3. https://docs.unity3d.com/ScriptReference/Object.DestroyImmediate.html