簡體   English   中英

在引用的序列化 ScriptableObject 中訂閱 c# 事件時,如何避免 Unity 中的 memory 泄漏?

[英]How do I avoid memory leak in Unity when subscribing to c# event in referenced serialized ScriptableObject?

在使用 Unity 實現我的游戲期間,我遇到了以下設置:

  • 我有一個具有 c# event委托的ScriptableObject (作為資產)。
  • 我有一個MonoBehaviour ,它具有對具有委托的ScriptableObject的序列化引用。

我想“訂閱” MonoBehaviour到該ScriptableObject的事件,正確處理該事件以避免 memory 泄漏。 最初我認為在OnEnable回調上訂閱事件並在OnDisable上取消訂閱就足夠了。 但是,當開發人員使用 Unity Inspector 在播放期間將序列化引用的值交換到ScriptableObject時,就會發生 memory 泄漏。

鑒於我希望游戲的開發人員能夠在游戲過程中交換檢查器中的值,是否有一種規范的方法可以安全地訂閱和取消訂閱 c# 事件的序列化引用中的ScriptableObject


為了說明這一點,我為該場景編寫了一個簡單的代碼:

SubjectSO.cs (帶有事件的ScriptableObject

using UnityEngine;
using System;

[CreateAssetMenu]
public class SubjectSO : ScriptableObject
{
    public event Action<string> OnTrigger;

    public void Invoke()
    {
        this.OnTrigger?.Invoke(this.name);
    }
}

ObserverMB.cs (想要訂閱ScriptableObject中的事件的MonoBehaviour

using UnityEngine;

public class ObserverMB : MonoBehaviour
{
    public SubjectSO subjectSO;

    public void OnEnable()
    {
        if(this.subjectSO != null)
        {
            this.subjectSO.OnTrigger += this.OnTriggerCallback;
        }
    }

    public void OnDisable()
    {
        if(this.subjectSO != null)
        {
            this.subjectSO.OnTrigger -= this.OnTriggerCallback;
        }
    }

    public void OnTriggerCallback(string value)
    {
        Debug.Log("Callback Received! Value = " + value);
    }
}

InvokesSubjectSOEveryUpdate.cs (輔助MonoBehaviour ,用於測試)

using UnityEngine;

public class InvokesSubjectSOEveryUpdate : MonoBehaviour
{
    public SubjectSO subjectSO;

    public void Update()
    {
        this.subjectSO?.Invoke();
    }
}

為了測試,我創建了兩個SubjectSO類型的資產,命名為:

  • 科目A
  • 科目B

然后,我在場景中創建了一個GameObject ,並附加了以下組件:

  • ObserverMB , 引用SubjectA
  • InvokesSubjectSOEveryUpdate ,引用SubjectA
  • InvokesSubjectSOEveryUpdate ,引用SubjectB

點擊播放時, Callback Received! Value = SubjectA 每次更新都會在控制台中打印Callback Received! Value = SubjectA ,這是預期的。

然后,當我使用檢查器將ObserverMB中的引用從SubjectA更改為SubjectB時,當游戲還在玩時,消息Callback Received! Value = SubjectA Callback Received! Value = SubjectA仍在繼續打印。

如果我在檢查器中禁用並啟用ObserverMB ,則兩條消息都Callback Received! Value = SubjectA Callback Received! Value = SubjectACallback Received! Value = SubjectB Callback Received! Value = SubjectB每次更新都開始打印。

最初的回調訂閱仍然有效,但是作為訂閱者, ObserverMB已經丟失了對該事件的引用。

我怎樣才能避免這種情況?

我真的相信這似乎是使用 c# event委托和ScriptableObjects的常見使用場景,而且OnEnableOnDisable沒有正確處理開發人員調整檢查器的序列化情況對我來說似乎很奇怪。

好吧,在這種情況下,您必須檢查subjectSO是否正在更改並取消訂閱。

通過 Inspector 切換值后,您的 class無法再取消訂閱之前的值。 因此,一開始訂閱的任何內容都將保持訂閱狀態。

用於檢查運行時

我會例如使用類似的屬性來做到這一點

// Make it private so no other script can directly change this
[SerializedField] private SubjectSO _currentSubjectSO;

// The value can only be changed using this property
// automatically calling HandleSubjectChange
public SubjectSO subjectSO
{
    get { return _currentSubjectSO; }
    set 
    {
        HandleSubjectChange(this._currentSubjectSO, value);
    }
}

private void HandleSubjectChange(SubjectSO oldSubject, SubjectSO newSubject)
{
    if (!this.isActiveAndEnabled) return;

    // If not null unsubscribe from the current subject
    if(oldSubject) oldSubject.OnTrigger -= this.OnTriggerCallback;

    // If not null subscribe to the new subject
    if(newSubject) 
    {
        newSubject.OnTrigger -= this.OnTriggerCallback;
        newSubject.OnTrigger += this.OnTriggerCallback;
    }

     // make the change
    _currentSubjectSO = newSubject;
}

所以每次其他腳本使用

observerMBReference.subject = XY;

它首先自動取消訂閱當前主題,然后訂閱新主題。


通過 Inspector 檢查更改

有兩種選擇:

通過Update方法和另一個支持字段,例如 go

#if UNITY_EDITOR
    private SubjectSO _previousSubjectSO;

    private void Update()
    {
        if(_previousSubjectSO != _currentSubjectSO)
        {
            HandleSubjectChange(_previousSubjectSO, _currentSubjectSO);
            _previousSubjectSO = _currentSubjectSO;
        }
    }
#endif

或者在OnValidate中做(感謝 zambari)同樣的事情

#if UNITY_EDITOR
    private SubjectSO _previousSubjectSO;

    // called when the component is created or changed via the Inspector
    private void OnValidate()
    {
        if(!Apllication.isPlaying) return;

        if(_previousSubjectSO != _currentSubjectSO)
        {
            HandleSubjectChange(_previousSubjectSO, _currentSubjectSO);
            _previousSubjectSO = _currentSubjectSO;
        }
    }
#endif

或者 - 因為這只會發生在通過檢查器更改字段的情況下 - 您可以實現一個Cutsom 編輯器,它僅在字段更改的情況下執行。 這設置起來有點復雜,但效率更高,因為在構建的后期,無論如何您都不需要Update方法。

通常您將編輯器腳本放在一個名為Editor的單獨文件夾中,但我個人認為將其實現到相應的 class 本身是一種很好的做法。

優點是這樣你也可以訪問private方法。 這樣你就可以自動知道 Inspector 有一些額外的行為。

#if UNITY_EDITOR
    using UnityEditor;
#endif

    ...

    public class ObserverMB : MonoBehaviour
    {
        [SerializeField] private SubjectSO _currentSubjectSO;
        public SubjectSO subjectSO
        {
            get { return _currentSubjectSO; }
            set 
            {
                HandleSubjectChange(_currentSubjectSO, value);
            }
        }

        private void HandleSubjectChange(Subject oldSubject, SubjectSO newSubject)
        {
            // If not null unsubscribe from the current subject
            if(oldSubject) oldSubject.OnTrigger -= this.OnTriggerCallback;

            // If not null subscribe to the new subject
            if(newSubject) newSubject.OnTrigger += this.OnTriggerCallback;

            // make the change
            _currentSubjectSO = newSubject;
        }

        public void OnEnable()
        {
            if(subjectSO) 
            {
                // I recommend to always use -= before using +=
                // This is allowed even if the callback wasn't added before
                // but makes sure it is added only exactly once!
                subjectSO.OnTrigger -= this.OnTriggerCallback;
                subjectSO.OnTrigger += this.OnTriggerCallback;
            }
        }

        public void OnDisable()
        {
            if(this.subjectSO != null)
            {
                this.subjectSO.OnTrigger -= this.OnTriggerCallback;
            }
        }

        public void OnTriggerCallback(string value)
        {
            Debug.Log("Callback Received! Value = " + value);
        }

#if UNITY_EDITOR
        [CustomEditor(typeof(ObserverMB))]
        private class ObserverMBEditor : Editor 
        { 
            private ObserverMB observerMB;
            private SerializedProperty subject;

            private Object currentValue;

            private void OnEnable()
            {
                observerMB = (ObserverMB)target;
                subject = serializedObject.FindProperty("_currentSubjectSO");
            }

            // This is kind of the update method for Inspector scripts
            public override void OnInspectorGUI()
            {
                // fetches the values from the real target class into the serialized one
                serializedObject.Update();

                EditorGUI.BeginChangeCheck();
                {
                    EditorGUILayout.PropertyField(subject);
                }
                if(EditorGUI.EndChangeCheck() && EditorApplication.isPlaying)
                {
                    // compare and eventually call the handle method
                    if(subject.objectReferenceValue != currentValue) observerMB.HandleSubjectChange(currentValue, (SubjectSO)subject.objectReferenceValue);
                }
            }
        }
#endif
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM