为什么使用消息机制
在Unity中Hierarchy面板中,若A的子级是B,A可以通过transform.Find去找到B所在的组件
但如果B要去访问A的组件,我们应该怎么办呢?
Unity采用了消息机制的方案,但是由于使用了反射,性能会很差。
所以我们可以自己封装一个消息机制。
一个简易的消息机制
原理:用字典存储事件。注册时,将事件添加到字典中。撤销时,从字典中把对应移除。使用时,直接从字典中找出来并调用。
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework
{
public class Example : MonoBehaviour
{
private static Dictionary<string, Action<object>> _notifyMsg = new Dictionary<string, Action<object>>();
public static void Register(string key, Action<object> callback)
{
_notifyMsg[key] = callback;
}
public static void Unregister(string key)
{
_notifyMsg.Remove(key);
}
public static void SendMsg(string key, object obj)
{
_notifyMsg[key]?.Invoke(obj);
}
private void Start()
{
Example.Register("消息1", (obj) =>
{
Debug.LogFormat("消息;{0}", obj);
});
Example.SendMsg("消息1", "hello world"); // 输出:消息;hello world
Example.Unregister("消息1");
Example.SendMsg("消息1", "hello"); // 报错:KeyNotFoundException: The given key '消息1' was not present in the dictionary.
}
}
}
进一步完善
在上面的代码中,我们会遇到两个问题,第一个问题是不能重复注册。第二个问题就是不能重复撤销。
问题的核心点,是因为字典的键唯一。怎么解决呢?
那就是通过事件的 += 和 -= 注册
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework
{
public class Example : MonoBehaviour
{
private static Dictionary<string, Action<object>> _registeredMsgs = new Dictionary<string, Action<object>>();
public static void Register(string msgName, Action<object> onMsgReceived)
{
if (!_registeredMsgs.ContainsKey(msgName))
{ // 第一次进来,设置一个空函数
_registeredMsgs[msgName] = _ => { };
}
_registeredMsgs[msgName] += onMsgReceived;
}
public static void UnRegisterAll(string msgName)
{ // 注销所有注册的事件
_registeredMsgs.Remove(msgName);
}
public static void UnRegister(string msgName, Action<object> onMsgReceived)
{ // 注册指定的事件
if (_registeredMsgs.ContainsKey(msgName))
{
_registeredMsgs[msgName] -= onMsgReceived;
}
}
public static void SendMsg(string key, object obj)
{
_registeredMsgs[key]?.Invoke(obj);
}
private void Start()
{
Example.Register("消息1", OnMsgReceived);
Example.Register("消息1", OnMsgReceived);
Example.SendMsg("消息1", "hello world"); // 输出:消息1:hello world 消息1:hello world (打印两遍)
Example.UnRegister("消息1", OnMsgReceived);
Example.SendMsg("消息1", "hello"); // 注册了一个,还有一个 输出:消息1:hello
}
public void OnMsgReceived(object data)
{
Debug.LogFormat("消息1:{0}", data);
}
}
}
如何用到框架中
using System;
using System.Collections.Generic;
namespace DYFramework
{
public class MsgDispatcher
{
private static Dictionary<string, Action<object>> _registeredMsgs = new Dictionary<string, Action<object>>();
public static void Register(string msgName, Action<object> onMsgReceived)
{
if (!_registeredMsgs.ContainsKey(msgName))
{ // 第一次进来,设置一个空函数
_registeredMsgs[msgName] = _ => { };
}
_registeredMsgs[msgName] += onMsgReceived;
}
public static void UnRegisterAll(string msgName)
{ // 注销所有注册的事件
_registeredMsgs.Remove(msgName);
}
public static void UnRegister(string msgName, Action<object> onMsgReceived)
{ // 注册指定的事件
if (_registeredMsgs.ContainsKey(msgName))
{
_registeredMsgs[msgName] -= onMsgReceived;
}
}
public static void SendMsg(string key, object obj)
{
_registeredMsgs[key]?.Invoke(obj);
}
}
}
但是这种注册不灵活,因为只能注册带参object的方法,如果想要注册一个无参的方法
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework
{
public abstract partial class MonoBehaviorSimplify : MonoBehaviour
{
Dictionary<string, Action<object>> _mMsgRegisterRecorder = new Dictionary<string, Action<object>>();
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
// 注册到消息中
MsgDispatcher.Register(msgName, onMsgReceived);
// 这一步的操作是添加到一个新的字典中,在脚本销毁的时候不需要写太多的注销方法(懒人专用)
_mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
private void OnDestroy()
{
// 这样操作,其他脚本继承了MonoBehaviorSimplify就无法使用OnDestroy()方法了,怎么办?
OnBeforeDestroy(); // 添加一个抽象方法
foreach (var record in _mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(record.Key, record.Value);
}
_mMsgRegisterRecorder.Clear();
}
// 如果方法中添加了abstract关键子,那么类也要添加abstract关键字,不然会报错
protected abstract void OnBeforeDestroy();
}
}
如何用到框架中2--进一步完善(使用列表)
我们是使⽤字典进⾏注册消息的记录的,使⽤字典就要保证字典中的 key 是唯⼀的。⽽我们很可能在⼀个脚本中对⼀个key注册多次,这样⽤字典这个数据结构就显得不合理了。
相⽐字典,List 更合适,因为我们有可能有重复的内容,⽽字典更适合做⼀些查询⼯作,但是 List 并不⽀持键值对,怎么办呢?
封装一个类
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework
{
/// <summary>
/// 集成消息机制
/// </summary>
public abstract partial class MonoBehaviorSimplify : MonoBehaviour
{
List<MsgRecord> _mMsgRegisterRecorder = new List<MsgRecord>();
private class MsgRecord
{
public string Name;
public Action<object> OnMsgReceived;
}
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
_mMsgRegisterRecorder.Add(new MsgRecord() { Name = msgName, OnMsgReceived = onMsgReceived });
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var record in _mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
}
_mMsgRegisterRecorder.Clear();
}
protected abstract void OnBeforeDestroy();
}
}
使用
using UnityEngine;
namespace DYFramework
{
public class Example : MonoBehaviorSimplify
{
private void Start()
{
RegisterMsg("DO", OnMsgReceived);
RegisterMsg("DO", OnMsgReceived);
RegisterMsg("DO", OnMsgReceived);
MsgDispatcher.SendMsg("DO", "hellowo"); // 输出三次 hellowo
}
public void OnMsgReceived(object data)
{
Debug.LogFormat("消息1:{0}", data);
}
protected override void OnBeforeDestroy()
{
}
}
}
如何用到框架中2--进一步完善(添加对象池)
我们每次注册消息,都要 new ⼀个 MsgRecord 对象出来。⽽我们在注销的时候,对这个对象是什么都没有做的。
这样会造成⼀个性能问题,这个性能问题主要是有 new 时候寻址造成的(也会增加GC的压力)
我们要做的,就是减少 new 的发⽣次数,要想减少,就得让我们的MsgRecord 能够回收利⽤
所以我们准备一个对象池。
如果对象池中没有,再去创建。如果对象池有,那就从对象池中拿。在销毁时,放回对象池中
using System;
using System.Collections.Generic;
using UnityEngine;
namespace DYFramework
{
/// <summary>
/// 集成消息机制
/// </summary>
public abstract partial class MonoBehaviorSimplify : MonoBehaviour
{
List<MsgRecord> _mMsgRegisterRecorder = new List<MsgRecord>();
private class MsgRecord
{
static readonly Stack<MsgRecord> _msgRecordPool = new Stack<MsgRecord>();
public string Name;
public Action<object> OnMsgReceived;
public static MsgRecord Allocate(string name, Action<object> onMsgReceived)
{
MsgRecord record = null;
if (_msgRecordPool.Count > 0)
{ // 对象池中有
record = _msgRecordPool.Pop();
}
else
{ // 对象池中没有在创建
record = new MsgRecord();
}
record.Name = name;
record.OnMsgReceived = onMsgReceived;
return record;
}
public void Recycle()
{
Name = null;
OnMsgReceived = null;
_msgRecordPool.Push(this);
}
}
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
_mMsgRegisterRecorder.Add(MsgRecord.Allocate(msgName, onMsgReceived));
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var record in _mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
record.Recycle();
}
_mMsgRegisterRecorder.Clear();
}
protected abstract void OnBeforeDestroy();
}
}
使用
using System.Collections;
using UnityEngine;
namespace DYFramework
{
public class Example : MonoBehaviorSimplify
{
private void Awake()
{
RegisterMsg("DO", OnMsgReceived);
RegisterMsg("DO", OnMsgReceived);
}
private IEnumerator Start()
{
MsgDispatcher.SendMsg("DO", "hellowo");
yield return new WaitForSeconds(1f);
MsgDispatcher.SendMsg("DO", "hellowo"); // 输出三次 hellowo
}
public void OnMsgReceived(object data)
{
Debug.LogFormat("消息1:{0}", data);
}
protected override void OnBeforeDestroy()
{
}
}
}
如何用到框架中3--进一步完善(遇到的两个问题)
第一个问题
注册消息,直接⽤ RegisterMsg,⽽注销则在 OnDestroy 的时候统⼀进⾏注销。那么单独注销时候怎么办呢?
解决思路:
注销对应键中的所有事件:先从列表中找出所有键名一样的,遍历它,移除字典中的并回收对象
public partial class MonoBehaviorSimplify : MonoBehaviour
{
/// <summary>
/// 注销这个键中的所有事件
/// </summary>
/// <param name="msgName">键名</param>
protected void UnRegisterMsg(string msgName)
{
List<MsgRecord> msgRecords = _mMsgRegisterRecorder.FindAll(record=> record.Name == msgName);
foreach (var record in msgRecords)
{
MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
_mMsgRegisterRecorder.Remove(record);
record.Recycle();
}
msgRecords.Clear();
}
/// <summary>
/// 注销对应键的指定事件
/// </summary>
/// <param name="msgName"></param>
/// <param name="onMsgReceived"></param>
protected void UnRegisterMsg(string msgName, Action<object> onMsgReceived)
{
var msgRecords = _mMsgRegisterRecorder.FindAll(record => record.Name == msgName && record.OnMsgReceived == onMsgReceived);
foreach (var record in msgRecords)
{
MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
_mMsgRegisterRecorder.Remove(record);
record.Recycle();
}
msgRecords.Clear();
}
}
第二个问题
第⼆个问题是 API 不统⼀的问题。这个问题要解决起来很简单。
protected void SendMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.SendMsg(msgName, onMsgReceived);
}
评论区