繁体   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