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

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

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

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

componentReference

測試用 Component

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

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 的執行狀況,這點在一般情況下是完全說得通的。 (前一篇文章 [C#] 關於 Delegate 的 GC 測試 – Testing if delegate will prevent an object to be GC 便是利用這點進行測試)

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

測試用腳本

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。

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

  1. 自行移除相關參照 的特性能否在輸出專案之後繼續成立,能否實際應用於開發上?
  2. 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
廣告

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s