[英]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.