基于特性标记的自动事件系统设计与实现
- 你是否受够了手动管理事件订阅的繁琐?
- 每次都要写一堆注册、解绑的重复代码,不仅容易遗漏,还得时刻提防内存泄漏。
- 是时候换个思路,把这一切自动化了。
本文深入分析了一个基于.NET反射机制和特性标记的自动化事件系统。该系统通过[Evt]特性标记方法,实现了事件订阅的自动化管理,采用对象池模式优化事件对象复用,并通过延迟队列机制解决了遍历期间修改集合的线程安全问题。本文将从设计思想、核心实现、性能优化及潜在问题等多个维度进行全面剖析。
一、系统概述
1.1 设计目标
在大型软件系统中,模块间通信是一个核心挑战。传统的事件系统需要手动注册/注销事件,容易导致以下问题:
- 订阅代码分散,维护困难
- 容易忘记取消订阅,造成内存泄漏
- 事件名称使用字符串常量,缺乏类型安全
本系统通过特性标记的方式,实现了声明式事件订阅,开发者只需在方法上标记[Evt]特性,系统会自动完成订阅注册。
csharp
旧版事件中心设计
public void OnEnable()
{
EventCenter.RegisterEvent(FunctionText);
}
public void OnDisable()
{
EventCenter.RemoveEvent(FunctionText);
}
public void FunctionText()
{
Debug.Log("原事件中心设计");
}
基于特性标记的事件系统
[Evt("EventName")]
public void FunctionText()
{
Debug.Log("基于特性标记的事件系统");
}
相比之下,基于特性标记的事件系统优势明显。它只需在函数上标记特性即可完成自动注册,不仅省去了手动注册与移除的繁琐步骤,还让开发流程更加流畅,维护成本更低。
二、核心组件详解
2.1 EvtAttribute - 事件标记特性
csharp
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class EvtAttribute : Attribute
{
public List<string> eventList { get; }
public EvtAttribute(params string[] events)
{
eventList = new List<string>(events);
}
}
设计要点:
- 支持一个方法订阅多个事件
- 支持在同一个方法上标记多个特性
- 事件名使用字符串,灵活性高但缺乏编译时检查
2.2 MvcEventVo - 事件封装器
MvcEventVo是整个系统的核心,封装了单个事件的所有订阅信息:
csharp
public class MvcEventVo
{
public MethodInfo MethodInfo; // 要执行的方法
public string EventName; // 事件名称
public int MethodParameterCount; // 参数数量(0或1)
private readonly object[] _sharedParams; // 复用参数数组
private readonly List<IEventInterester> _interesters; // 订阅者列表
private readonly List<IEventInterester> _todoAdd; // 延迟添加队列
private readonly List<IEventInterester> _todoRemove; // 延迟移除队列
private bool _isRunning; // 是否正在处理事件
}
2.2.1 延迟修改机制
在遍历_interesters期间,如果直接修改集合会抛出InvalidOperationException。本系统通过_todoAdd和_todoRemove队列解决了这个问题:
csharp
public void Handle(IEventX e)
{
_isRunning = true;
// 遍历执行所有订阅者
foreach (var interester in _interesters)
{
MethodInfo.Invoke(interester, _sharedParams);
}
// 处理延迟添加
if (_todoAdd.Count > 0)
{
foreach (var todo in _todoAdd)
_interesters.Add(todo);
_todoAdd.Clear();
}
// 处理延迟移除
if (_todoRemove.Count > 0)
{
foreach (var toRemove in _todoRemove)
_interesters.Remove(toRemove);
_todoRemove.Clear();
}
_isRunning = false;
}
这种设计保证了:
- 事件处理期间的修改不会影响当前遍历
- 修改会在下次事件触发时生效
- 避免了复杂的锁机制
2.2.2 参数数组复用
csharp
private readonly object[] _sharedParams = new object[1];
// 使用时只修改数组内容,不创建新数组
_sharedParams[0] = e;
MethodInfo.Invoke(interester, _sharedParams);
这种优化减少了每次事件触发时的GC分配,对于高频事件场景有明显性能提升。
2.3 EventX - 事件对象池
csharp
public class EventX : IEventX
{
private static readonly Queue<EventX> eventPool = new();
public static EventX FromPool(string name, object body = null, string type = null)
{
var e = eventPool.Count > 0 ? eventPool.Dequeue() : new EventX();
e.Reset(name, body, type);
return e;
}
public static void Recycle(EventX e)
{
if (eventPool.Count < 100)
{
e.Reset(null);
eventPool.Enqueue(e);
}
}
}
对象池模式的优势:
- 减少对象创建次数,降低GC压力
- 设置容量上限(100),避免内存无限增长
- 适用于频繁创建销毁的场景
2.4 反射注入器
csharp
private static void Inject(object injectable)
{
var contract = injectable.GetType();
// 双重检查,避免重复扫描
if (mvcEvents.TryGetValue(contract, out _) == false)
{
var methodist = mvcEvents[contract] = new List<MvcEventVo>();
var methods = contract.GetMethods(BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
foreach (var method in methods)
{
var attrs = method.GetCustomAttributes(typeof(EvtAttribute), false);
if (attrs.Length == 0) continue;
// 参数验证
var paramCount = method.GetParameters().Length;
if (paramCount > 0 &&
method.GetParameters()[0].ParameterType != typeof(IEventX))
{
Debug.LogError("事件注入参数有问题,应该为IEventX");
continue;
}
// 为每个事件名创建MvcEventVo
foreach (EvtAttribute attr in attrs)
{
foreach (var evt in attr.eventList)
{
methodist.Add(new MvcEventVo
{
EventName = evt,
MethodInfo = method,
MethodParameterCount = paramCount
});
}
}
}
}
}
扫描策略:
- 只扫描实例方法(包括私有方法)
- 方法可以有0或1个参数,参数必须是
IEventX类型 - 一个方法可以订阅多个事件
- 结果缓存到
mvcEvents字典,避免重复反射
三、使用示例
3.1 定义事件订阅者
csharp
public class UIManager : IEventInterester
{
// 无参数方法
[Evt(EventName.GameStart)]
private void OnGameStart()
{
Debug.Log("UI: 游戏开始");
ShowStartMenu();
}
// 带事件参数的方法
[Evt("ScoreChanged", "LevelUp")]
private void OnScoreChanged(IEventX e)
{
int newScore = (int)e.data;
UpdateScoreUI(newScore);
}
// 订阅多个事件
[Evt("NetworkConnected", "NetworkDisconnected")]
private void OnNetworkStatusChanged(IEventX e)
{
bool isConnected = e.Name == "NetworkConnected";
ShowNetworkStatus(isConnected);
}
}
3.2 发布事件
csharp
// 获取事件对象
var evt = EventX.FromPool("ScoreChanged", 100);
// 发布事件
Facade.Facade.Instance.Emit(evt);
// 回收事件对象
EventX.Recycle(evt);
3.3 完整的生命周期管理
csharp
public class PlayerController : IEventInterester
{
private List<MvcEventVo> _subscribedEvents;
public PlayerController()
{
// 自动订阅事件
_subscribedEvents = EventHelper.GetEventInterests(this);
}
[Evt("PlayerDead")]
private void OnPlayerDead()
{
Debug.Log("玩家死亡");
}
public void OnDestroy()
{
// 取消所有订阅,防止内存泄漏
foreach (var evt in _subscribedEvents)
{
evt.RemoveTarget(this);
}
}
}