基于特性标记的事件系统,优雅解决事件注册的繁琐过程

12 阅读4分钟

基于特性标记的自动事件系统设计与实现

  • 你是否受够了手动管理事件订阅的繁琐?
  • 每次都要写一堆注册、解绑的重复代码,不仅容易遗漏,还得时刻提防内存泄漏。
  • 是时候换个思路,把这一切自动化了。

本文深入分析了一个基于.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);
        }
    }
}