うにてぃブログ

主にUnityとC#に関する記事を書いていきます

【Unity】UnityEvent で 利用した Delegate は初期化しないとリークする

デリゲートを利用してスクリプトを記述することはよくあるが、UnityEvent のリスナーに登録してる場合に破棄されたタイミングでメモリに残ってしまい、GC.Collect を行っても回収されないようです

実際に試してみる

※ Unity2020.3.14f1 UnityEditor上 Memory Profilerで確認

デリゲートを利用するクラスを記述し、インスタンスを作成したのちすぐ削除してみてる

public class SampleMonoBehaviour : MonoBehaviour
{
    [SerializeField]
    private DelegateCheck _check;
      
    private IEnumerator Start()
    {
        for (int i = 0; i < 10; i++)
        {
            var instance = Instantiate(_check, transform);
            instance.Delegate += Event;
            yield return null;
            Destroy(instance);
        }
 
        GC.Collect();
    }
 
    private void Event(DelegateCheck.DelegateEvent de)
    {
        Debug.Log("log");
    }
}
 
public class DelegateCheck : MonoBehaviour
{
    public class DelegateEvent { }
    public event Action<DelegateEvent> Delegate;
 
    private void Awake()
    {
        var button = gameObject.AddComponent<Button>();
        button.onClick.AddListener(() => Delegate?.Invoke(new DelegateEvent()));
    }
}

Memory Profiler で中身を確認すると、インスタンスは破棄しているはずなのに残っていることがわかる

削除時に初期化処理を追記する

先程のクラスに削除されたタイミングで初期化を行う処理を追加して確認してみる

public class DelegateCheck : MonoBehaviour
{
    private void OnDestroy()
    {
        Delegate = null;
    }
}

するとメモリには残ってないことが確認できる

しかしこれは button.onClick.RemoveAllListeners() でも同じくメモリから開放することができているので、UnityEvent の リスナーに登録した場合参照が残っているため消えないというのが正しそうです

そのため、以下のように AddListener せず Delegate に登録して呼び出すだけであればメモリに残ることはありませんでした

public class DelegateCheck : MonoBehaviour
{
    public class DelegateEvent { }
    public event Action<DelegateEvent> Delegate;
 
    public void Invoke()
    {
        Delegate.Invoke(new DelegateEvent());   
    }

PS.

デリゲートじゃなくて直接関数を登録した場合でも同じく残っていました

    _button.onClick.AddListener(Click);
 
    public void Click()
    {
        Delegate.Invoke(new DelegateEvent());   
    }

またラムダや実機の場合に挙動が違うみたいなので、時間があるときにでももう少し詳しく調べようと思います