前言

发布订阅模式很常见,每个发布者和订阅者之间都搭建了一条小线路,随着功能越来越多,事件和委托就会满天飞,就像私拉电线的蜘蛛网一样。这时候可能需要一种集中式的事件处理方法,即事件总线。

1,简介

事件总线就像一个集线器,原本直接从起点到终点的连接,如今全部都要经过事件总线,发布者和订阅者完全地解耦了。发布者只需要向事件总线发起事件,不需要关心事件处理。订阅者只需要处理事件总线派发过来的事件,不需要关心事件的来源。

2,设计

2.1 设计思路

我希望事件总线是简单整洁灵活的

  • 定义一个事件接口IEventData,所有的事件类都应该继承此接口,用不同的类型代表不同的事件,并且事件类包含了全部事件信息。
  • 事件总线维护一个字典Dictionary<Type, List<Action<IEventData>>,第一个泛型参数Type表示事件类型,第二个泛型参数List<Action<IEventData>>表示事件处理委托列表。(此处用Action举例,Func是类似的)
  • 订阅者手动向事件总线注册事件处理委托
  • 发布者创建一个IEventData实例即可向事件总线触发事件,把事件处理委托列表调用一遍

2.2 设计实现

2.2.1 IEventData

//事件接口,所有的事件都要实现该接口
public interface IEventData
{
}

一个空接口就行,应用时再根据业务定义事件类

2.2.2 EventBus

public class EventBus
{
public static EventBus Default = new EventBus(); //单例 private readonly Dictionary<Type, List<object>> eventDataAndActionHandlerDic; //Action<IEventData>
private static readonly object 字典锁 = new object(); private EventBus()
{
eventDataAndActionHandlerDic = new Dictionary<Type, List<object>>();
} //手动注册事件处理方法
public void Register<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (!eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
eventDataAndActionHandlerDic.Add(typeof(TEventData), new List<object>());
}
List<object> actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
if (!actionList.Contains(action))
{
actionList.Add(action);
}
}
} //手动注销事件处理方法
public void UnRegister<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
List<object> actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
actionList.Remove(action);
}
}
} //触发事件
public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
{
if (eventData == null)
{
throw new ArgumentNullException(nameof(eventData));
}
List<object> actionList = null;
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
}
}
if(actionList != null)
{
for (var index = 0; index < actionList.Count; index++)
{
actionList[index].GetType().GetMethod("Invoke").Invoke(actionList[index], new object[] { eventData });
}
}
}
}

很简单,就3个方法(此处用Action举例,Func是类似的)

  • Register:将订阅者的事件处理委托添加到事件处理委托列表
  • UnRegister:将订阅者的事件处理委托从事件处理委托列表里移除
  • Trigger:根据事件类型从字典里拿到事件处理委托列表都调用一遍

2.2.3 用起来

假设一个应用场景,一个气象台发布天气,多个电视台接收天气。

创建一个winform项目。

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void 订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.Register<EventData<string>>(中国电视台.收到天气);
EventBus.Default.Register<EventData<string>>(米国电视台.收到天气);
}
private void 取消订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.UnRegister<EventData<string>>(中国电视台.收到天气);
EventBus.Default.UnRegister<EventData<string>>(米国电视台.收到天气);
} private 气象台 气象台 = new 气象台();
private void 播报天气_Click(object sender, EventArgs e)
{
气象台.播报天气("下雨");
}
} //带泛型负载的事件
public class EventData<TPayload> : IEventData
{
public TPayload Payload { get; protected set; } public EventData(TPayload payload)
{
Payload = payload;
}
}
public class 气象台
{
public void 播报天气(string 天气)
{
EventData<string> eventData = new EventData<string>(天气);
EventBus.Default.Trigger<EventData<string>>(eventData); //直接通过事件总线触发即可
}
}
public class 中国电视台
{
public static void 收到天气(EventData<string> eventData)
{
MessageBox.Show($"中国天气是{eventData.Payload}");
}
}
public class 米国电视台
{
public static void 收到天气(EventData<string> eventData)
{
MessageBox.Show($"米国天气是{eventData.Payload}");
}
}

根据业务定义了一个带泛型负载的事件EventData<TPayload>。发布者气象台可以通过Trigger方法发起事件。订阅者电视台通过Register方法注册事件处理委托。先点击订阅天气,再点击播报天气时就会弹窗。

3,问题

3.1 起缘

一切都看起来很棒,直到有一天电视台要拆掉了。

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
} private 中国电视台 中国电视台 = new 中国电视台();
private 米国电视台 米国电视台 = new 米国电视台();
private void 订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.Register<天气数据, string>(中国电视台.收到天气);
EventBus.Default.Register<天气数据, string>(米国电视台.收到天气);
}
private void 取消订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.UnRegister<天气数据, string>(中国电视台.收到天气);
EventBus.Default.UnRegister<天气数据, string>(米国电视台.收到天气);
}
private void 销毁电视台_Click(object sender, EventArgs e)
{
//如果没有取消订阅,内存就泄露了
中国电视台 = null;
米国电视台 = null;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
textBox1.AppendText($"销毁电视台\r\n");
} private 气象台 气象台 = new 气象台();
private void 播报天气_Click(object sender, EventArgs e)
{
气象台.播报天气("下雪", 播报天气CallBack);
}
private void 播报天气CallBack(string obj)
{
textBox1.AppendText($"{obj}\r\n");
}
} //带负载的事件
public class 天气数据: IEventData
{
public string 天气;
public 天气数据(string 天气)
{
this.天气 = 天气;
}
} public class 气象台
{
public void 播报天气(string 天气, Action<string> callBack)
{
天气数据 eventData = new 天气数据(天气);
List<string> list = EventBus.Default.Trigger<天气数据, string>(eventData); //直接通过事件总线触发即可
foreach(var str in list)
{
callBack?.Invoke(str);
}
}
}
public class 中国电视台
{
public string 收到天气(天气数据 eventData)
{
return $"中国电视台收到的天气是{eventData.天气}";
}
}
public class 米国电视台
{
public string 收到天气(天气数据 eventData)
{
return $"米国电视台收到的天气是{eventData.天气}";
}
}

先点击订阅天气,再点击播报天气时就会显示天气,这都没问题。此时再点击销毁电视台,再点击播报天气时仍然会显示天气,这是一个常见的内存泄露。



销毁电视台前手动注销事件处理委托可以避免这个问题,但这真的很难保证。有没有一种即便不手动注销也能正常GC的方法呢?有,就是弱引用。这个问题本来和事件总线没啥关系,但如果用到了弱引用就需要对事件总线进行改造。

3.2 改造

注册了事件处理委托之后,事件总线对订阅者就是强引用关系,强引用在,GC永远无法回收被引用者。弱引用不计入引用计数,引用计数归零GC可以正常回收。弱引用在使用时先判断对象是否存在,如果存在才访问对象。

下面将事件总结对事件处理委托的引用改造成弱引用。更确切地说,就是使用弱引用对Dictionary<Type, List<Action<IEventData>>中的List<Action<IEventData>>进行封装。

public class WeakEvent<T>
{
private class ActionUnit //弱引用封装
{
private WeakReference reference;
private MethodInfo method;
private bool noTarget; public bool IsDead
{
get
{
return !this.noTarget && !this.reference.IsAlive;
}
}
public ActionUnit(Action<T> action)
{
this.noTarget = action.Target == null; //静态方法没有Target,所以noTarget就是isStaticMethod
this.reference = new WeakReference(action.Target); //action.Target是订阅者实例
this.method = action.Method; //Method是MethodInfo的实例,即订阅者的事件处理方法
}
public bool Equals(Action<T> action)
{
return this.reference.Target == action.Target && this.method == action.Method;
}
public void Invoke(object[] args)
{
this.method.Invoke(this.reference.Target, args); //reference.Target就是action.Target,WeakReference构造函数中传入的
}
} private List<ActionUnit> actionUnitlist = new List<ActionUnit>(); //弱引用封装列表 public int Count
{
get
{
return this.actionUnitlist.Count;
}
} public void Add(Action<T> action)
{
this.actionUnitlist.Add(new ActionUnit(action));
}
public void Remove(Action<T> action)
{
for (int i = this.actionUnitlist.Count - 1; i > -1; i--)
{
if (this.actionUnitlist[i].Equals(action))
{
this.actionUnitlist.RemoveAt(i);
}
}
}
public void Clear()
{
this.actionUnitlist.Clear();
}
public bool Contains(Action<T> action)
{
return this.actionUnitlist.Any(item => item.Equals(action));
} public void Invoke(T arg)
{
List<int> removeList = new List<int>();
for (int i = 0; i < this.actionUnitlist.Count; i++)
{
if (this.actionUnitlist[i].IsDead)
{
removeList.Add(i);
}
else
{
this.actionUnitlist[i].Invoke(new object[] { arg });
}
}
for (int i = removeList.Count - 1; i >= 0; i--)
{
this.actionUnitlist.RemoveAt(removeList[i]);
}
}
}

ActionUnit对应的是Action<TEventData>WeakEvent<T>对应的是List<Action<TEventData>>

添加事件处理委托action时,就使用弱引用表示订阅者new WeakReference(action.Target),这样就不会增加订阅者的引用计数。在调用事件处理委托时,会逐个判断订阅者是否存在,存在则调用事件处理委托,不存在的就从列表中移除掉。

public class EventBus
{
public static EventBus Default = new EventBus(); private readonly Dictionary<Type, object> eventDataAndActionHandlerDic;
private static readonly object 字典锁 = new object(); private EventBus()
{
eventDataAndActionHandlerDic = new Dictionary<Type, object>();
} //手动注册事件处理方法
public void Register<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (!eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
eventDataAndActionHandlerDic.Add(typeof(TEventData), new WeakEvent<TEventData>());
}
object actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
WeakEvent<TEventData> weakEvent = (WeakEvent<TEventData>)actionList;
if (!weakEvent.Contains(action))
{
weakEvent.Add(action);
}
}
} //手动注销事件处理方法
public void UnRegister<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
object actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
((WeakEvent<TEventData>)actionList).Remove(action);
}
}
} //触发事件
public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
{
if (eventData == null)
{
throw new ArgumentNullException(nameof(eventData));
}
object actionList = null;
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
}
}
if(actionList != null)
{
((WeakEvent<TEventData>)actionList).Invoke(eventData);
}
}
}

EventBus和之前基本差不多,仅将List<Action<TEventData>>替换成WeakEvent<TEventData>

3.3 用起来

业务代码是一样的,结果就是销毁电视台就真的销毁了,再播报也不会显示天气了。

C#应用 - 事件总线的更多相关文章

  1. Android事件总线

    Android中Activity.Service.Fragment之间的相互通信比较麻烦,主要有以下一些方法: (1)使用广播,发送者发出广播,接收者接收广播后进行处理: (2)使用Handler和M ...

  2. ABP理论学习之事件总线和领域事件

    返回总目录 本篇目录 事件总线 定义事件 触发事件 处理事件 句柄注册 取消注册 在C#中,我们可以在一个类中定义自己的事件,而其他的类可以注册该事件,当某些事情发生时,可以通知到该类.这对于桌面应用 ...

  3. Lind.DDD.Events事件总线~自动化注册

    回到目录 让大叔兴奋的自动化注册 对于领域事件之前说过,在程序启动时订阅(注册)一些事件处理程序,然后在程序的具体位置去发布(触发)它,这是传统的pub/sub模式的体现,当然也没有什么问题,为了让它 ...

  4. Guava - EventBus(事件总线)

    Guava在guava-libraries中为我们提供了事件总线EventBus库,它是事件发布订阅模式的实现,让我们能在领域驱动设计(DDD)中以事件的弱引用本质对我们的模块和领域边界很好的解耦设计 ...

  5. DDD~领域事件与事件总线

    回到目录 谈谈它 终于有些眉目了,搜刮了很多牛人的资料,英文的,中文的,民国文的,终于小有成就了,同时也做了个DEMO,领域事件这东西好,但需要你明白它之后才会说好,而对于明白领域事件这件事来说,它的 ...

  6. Android学习系列(43)--使用事件总线框架EventBus和Otto

    事件总线框架 针对事件提供统一订阅,发布以达到组件间通信的解决方案. 原理 观察者模式. EventBus和Otto 先看EventBus的官方定义: Android optimized event ...

  7. ASP.NET ZERO 学习 事件总线

    用于注册和触发客户端的全局事件. 介绍 Pub/sub事件模型广泛用于客户端,ABP包含了一个简单的全局事件总线来 注册并 触发事件. 注册事件 可以使用abp.event.on来注册一个全局事件.一 ...

  8. 【DDD-Apwork框架】事件总线和事件聚合器

    第一步:事件总线和事件聚合器 [1]事件总线 IEventBus IUnitOfWork.cs using System; using System.Collections.Generic; usin ...

  9. AndroidEventBus ( 事件总线 ) 的设计与实现

    1. 功能介绍 AndroidEventBus是一个Android平台的事件总线库, 它简化了Activity.Fragment.Service等组件或者对象之间的交互,非常大程度上减少了它们之间的耦 ...

  10. Guava: 事件总线EventBus

    EventBus 直译过来就是事件总线,它使用发布订阅模式支持组件之间的通信,不需要显式地注册回调,比观察者模式更灵活,可用于替换Java中传统的事件监听模式,EventBus的作用就是解耦,它不是通 ...

随机推荐

  1. 高通Android平台 电池 相关配置

    背景 在新基线上移植有关的代码时,在log中发现有关的东西,请教了有关的同事以后,解决了这个问题. [ 12.775863] pmi632_charger: smblib_eval_chg_termi ...

  2. 【资料分享】全志科技T507工业核心板硬件说明书(下)

    目    录 3 电气特性 3.1 工作环境 3.2 功耗测试 3.3 热成像图 4 机械尺寸 5 底板设计注意事项 5.1 最小系统设计 5.1.1 电源设计说明 5.1.2 系统启动配置 5.1. ...

  3. pyside6 QThread 以及自定义信号 测试

    import sys import random from time import sleep from PySide6 import QtCore as qc from PySide6 import ...

  4. SpringBoot获取请求头信息

    Http 头信息 HTTP 头(Header)是一种附加内容,独立于请求内容和响应内容. HTTP 协议中的大量特性都通过Header信息交互来实现,比如内容编解码.缓存.连接保活等等. reques ...

  5. cf edu 133 D

    题意 思路 根据题意,最开始可以想到一个二维的dp状态 用dp[i][j]表示跳了j次刚好到i的方案数 如果是跳了j次,那么这次应该要被k+j-1整除才行 那么这样状态转移就是 dp[i][j] = ...

  6. MySQL_数据库命名规范及约定

    操作规范 如无说明,建表时一律采用innodb引擎: 如无说明,数据库表编码集(utf8,utf8_bin)ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf ...

  7. 音频文件降噪及python示例

    操作系统 :Windows 10_x64 Python版本:3.9.2 noisereduce版本:3.0.2 从事音频相关工作,大概率会碰到降噪问题,今天整理下之前学习音频文件降噪的笔记,并提供Au ...

  8. 不是,大哥,咱这小门小户的,别搞我CDN流量啊

    分享是最有效的学习方式. 博客:https://blog.ktdaddy.com/ 最近遇上事儿了,老猫的小小博客网站[程序员老猫的博客]被人盗刷CDN流量了.开始的时候收到了欠费的短信也没有怎么去重 ...

  9. springboot简单正确的使用构造函数注入

    一个一个写构造函数太麻烦了,而且代码会显得非常多,这里我们可以采用lombok快捷注入 但是我们并不是所有的成员变量都需要进行注入,所以使用 @RequiredArgsConstrucotr 需要构造 ...

  10. 【H5】02 <head>头标签介绍

    摘自: https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML/The_head_metadata_in_HT ...