[英]How do I avoid memory leak in Unity when subscribing to c# event in referenced serialized ScriptableObject?
在使用 Unity 实现我的游戏期间,我遇到了以下设置:
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
类型的资产,命名为:
然后,我在场景中创建了一个GameObject
,并附加了以下组件:
ObserverMB
, 引用SubjectAInvokesSubjectSOEveryUpdate
,引用SubjectAInvokesSubjectSOEveryUpdate
,引用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 = SubjectA
并Callback Received! Value = SubjectB
Callback Received! Value = SubjectB
每次更新都开始打印。
最初的回调订阅仍然有效,但是作为订阅者, ObserverMB
已经丢失了对该事件的引用。
我怎样才能避免这种情况?
我真的相信这似乎是使用 c# event
委托和ScriptableObjects
的常见使用场景,而且OnEnable
和OnDisable
没有正确处理开发人员调整检查器的序列化情况对我来说似乎很奇怪。
好吧,在这种情况下,您必须检查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;
它首先自动取消订阅当前主题,然后订阅新主题。
有两种选择:
通过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.