From 8acccdcc4b1f0c51fc864e1d37d82d51f586348e Mon Sep 17 00:00:00 2001 From: cneicy Date: Thu, 18 Jun 2026 17:05:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E6=80=BB=E7=BA=BF=E4=B8=BA=E5=8F=AF=E5=AE=9E=E4=BE=8B=E5=8C=96?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E5=B9=B6=E5=8F=91=E5=B8=83=201.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 IShrinkEventBus、ShrinkEventBusBuilder 与 ShrinkEventBusInstance,支持独立 bus、异常策略、启动开关、类型校验和按 phase 派发 - 新增 SubscribeEvent 可释放订阅句柄与订阅快照接口,保留 EventBus 静态门面并委托到默认实例 - 重构监听列表为父子事件链共享锁与快照缓存,支持父事件监听子事件、跨父子数字优先级归并和防御性快照拷贝 - 优化 EventBase 元数据缓存、懒生成 EventId、派发快照挂接与对象池双重归还保护,移除旧 EventCache 热路径 - 恢复本地 ILPostProcessor 兜底并生成静态订阅注册表,避免共享 CodeGen 覆盖外程序集自动注册失效 - 改进事件查看器、Benchmark、README 与 CHANGELOG,并新增 Dispatch/Priority/Registration/Subscription 测试覆盖核心行为 BREAKING CHANGE: 移除 EventBase.GetListenerList(),int 数字优先级重载不再提供默认值,数字优先级 0 现在映射为 NORMAL。 --- Benchmark.txt | 113 +-- CHANGELOG.md | 43 ++ CodeGen/Editor/EventBusILPostProcessor.cs | 333 ++++---- .../Editor/PostProcessorAssemblyResolver.cs | 52 +- .../PostProcessorAssemblyResolver.cs.meta | 4 +- .../Editor/PostProcessorReflectionImporter.cs | 21 +- .../PostProcessorReflectionImporter.cs.meta | 4 +- .../Unity.ShrinkEventBus.CodeGen.asmdef | 2 +- Editor/EventBusViewerWindow.cs | 113 ++- README.md | 176 ++++- Runtime/EventBase.cs | 69 +- Runtime/EventBus.cs | 479 +++--------- Runtime/EventBusBenchmark.cs | 349 +++++++-- Runtime/EventBusGeneratedRegistry.cs | 53 ++ Runtime/EventBusGeneratedRegistry.cs.meta | 11 + Runtime/EventBusRegHelper.cs | 239 ++++-- Runtime/EventCache.cs | 7 - Runtime/EventCache.cs.meta | 3 - Runtime/EventCloneUtility.cs | 16 +- Runtime/EventHandlerInfo.cs | 79 +- Runtime/EventPool.cs | 17 +- Runtime/EventPriority.cs | 2 +- Runtime/IShrinkEventBus.cs | 109 +++ Runtime/IShrinkEventBus.cs.meta | 11 + Runtime/ListenerList.cs | 225 ++++-- Runtime/ShrinkEventBusBuilder.cs | 76 ++ Runtime/ShrinkEventBusBuilder.cs.meta | 11 + Runtime/ShrinkEventBusInstance.cs | 708 ++++++++++++++++++ Runtime/ShrinkEventBusInstance.cs.meta | 11 + Tests.meta | 8 + Tests/EventBusDispatchTests.cs | 200 +++++ Tests/EventBusDispatchTests.cs.meta | 11 + Tests/EventBusPriorityTests.cs | 120 +++ Tests/EventBusPriorityTests.cs.meta | 11 + Tests/EventBusRegistrationTests.cs | 222 ++++++ Tests/EventBusRegistrationTests.cs.meta | 11 + Tests/EventBusSubscriptionTests.cs | 94 +++ Tests/EventBusSubscriptionTests.cs.meta | 11 + Tests/ShrinkEventBus.Tests.asmdef | 23 + Tests/ShrinkEventBus.Tests.asmdef.meta | 7 + package.json | 2 +- 41 files changed, 3168 insertions(+), 888 deletions(-) create mode 100644 Runtime/EventBusGeneratedRegistry.cs create mode 100644 Runtime/EventBusGeneratedRegistry.cs.meta delete mode 100644 Runtime/EventCache.cs delete mode 100644 Runtime/EventCache.cs.meta create mode 100644 Runtime/IShrinkEventBus.cs create mode 100644 Runtime/IShrinkEventBus.cs.meta create mode 100644 Runtime/ShrinkEventBusBuilder.cs create mode 100644 Runtime/ShrinkEventBusBuilder.cs.meta create mode 100644 Runtime/ShrinkEventBusInstance.cs create mode 100644 Runtime/ShrinkEventBusInstance.cs.meta create mode 100644 Tests.meta create mode 100644 Tests/EventBusDispatchTests.cs create mode 100644 Tests/EventBusDispatchTests.cs.meta create mode 100644 Tests/EventBusPriorityTests.cs create mode 100644 Tests/EventBusPriorityTests.cs.meta create mode 100644 Tests/EventBusRegistrationTests.cs create mode 100644 Tests/EventBusRegistrationTests.cs.meta create mode 100644 Tests/EventBusSubscriptionTests.cs create mode 100644 Tests/EventBusSubscriptionTests.cs.meta create mode 100644 Tests/ShrinkEventBus.Tests.asmdef create mode 100644 Tests/ShrinkEventBus.Tests.asmdef.meta diff --git a/Benchmark.txt b/Benchmark.txt index 799d811..fb2a369 100644 --- a/Benchmark.txt +++ b/Benchmark.txt @@ -1,68 +1,83 @@ -[Benchmark] 开始:无订阅者触发 -[Benchmark] 完成:无订阅者触发 -[Benchmark] 开始:单订阅者同步触发 -[Benchmark] 完成:单订阅者同步触发 -[Benchmark] 开始:30 订阅者同步触发 -[Benchmark] 完成:30 订阅者同步触发 -[Benchmark] 开始:异步触发 -[Benchmark] 完成:异步触发 -[Benchmark] 开始:注册/注销 -[Benchmark] 完成:RegisterEvent -[Benchmark] 完成:UnregisterEvent -[Benchmark] 开始:EventPool vs new -[Benchmark] 完成:new() 分配 -[Benchmark] 完成:Pool.Get() -[Benchmark] 开始:已取消事件跳过 -[Benchmark] 完成:已取消事件跳过 ╔══════════════════════════════════════════════════╗ ║ ShrinkEventBus Benchmark Report ║ -║ iter= 1000000 mass=10000 reg= 5000 cancel=100000 ║ +║ iter= 1000000 mass= 10000 reg= 5000 scan=1000 ║ ╚══════════════════════════════════════════════════╝ + 基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启) + 采集于 1.3.0(EventBase 瘦身后;Unity 2022.3.62f3, Editor Play Mode) ▶ TriggerEvent × 无订阅者(基线) - 总耗时 : 68.481 ms - 单次 : 0.0685 μs/op - 吞吐量 : 14602548 ops/s + 总耗时 : 111.466 ms + 单次 : 0.1115 μs/op + 吞吐量 : 8971378 ops/s ▶ TriggerEvent × 单订阅者同步 - 总耗时 : 115.262 ms - 单次 : 0.1153 μs/op - 吞吐量 : 8675871 ops/s + 总耗时 : 163.073 ms + 单次 : 0.1631 μs/op + 吞吐量 : 6132242 ops/s + + ▶ TriggerEvent × 父事件监听子事件 + 总耗时 : 171.424 ms + 单次 : 0.1714 μs/op + 吞吐量 : 5833479 ops/s ▶ TriggerEvent × 30 订阅者同步 - 总耗时 : 7.376 ms - 单次 : 0.7376 μs/op - 吞吐量 : 1355712 ops/s + 总耗时 : 38.639 ms + 单次 : 3.8639 μs/op + 吞吐量 : 258805 ops/s + + ▶ TriggerEvent(Phase=HIGH) × 3 phase 监听 + 总耗时 : 164.197 ms + 单次 : 0.1642 μs/op + 吞吐量 : 6090238 ops/s ▶ TriggerEventAsync × 单订阅者异步 - 总耗时 : 0.684 ms - 单次 : 0.6840 μs/op - 吞吐量 : 1461988 ops/s + 总耗时 : 0.856 ms + 单次 : 0.8557 μs/op + 吞吐量 : 1168634 ops/s - ▶ RegisterEvent - 总耗时 : 18.780 ms - 单次 : 3.7561 μs/op - 吞吐量 : 266236 ops/s + ▶ RegisterEvent(delegate) + 总耗时 : 14.418 ms + 单次 : 2.8836 μs/op + 吞吐量 : 346791 ops/s - ▶ UnregisterEvent - 总耗时 : 117.646 ms - 单次 : 23.5292 μs/op - 吞吐量 : 42500 ops/s + ▶ UnregisterEvent(delegate) + 总耗时 : 3.427 ms + 单次 : 0.6853 μs/op + 吞吐量 : 1459130 ops/s - ▶ TriggerEvent × new() [GC Gen0=6457] - 总耗时 : 7684.313 ms - 单次 : 7.6843 μs/op - 吞吐量 : 130135 ops/s + ▶ Register/Unregister(instance scan) + 总耗时 : 13.097 ms + 单次 : 13.0971 μs/op + 吞吐量 : 76353 ops/s - ▶ TriggerEvent × Pool.Get() [GC Gen0= 91] - 总耗时 : 348.948 ms - 单次 : 0.3489 μs/op - 吞吐量 : 2865761 ops/s + ▶ Register/Unregister(static type scan) + 总耗时 : 9.337 ms + 单次 : 9.3374 μs/op + 吞吐量 : 107096 ops/s + + ▶ Register/Unregister(MethodInfo) + 总耗时 : 6.513 ms + 单次 : 6.5134 μs/op + 吞吐量 : 153530 ops/s + + ▶ TriggerEvent × new() [GC Gen0= 1] + 总耗时 : 266.536 ms + 单次 : 0.2665 μs/op + 吞吐量 : 3751843 ops/s + + ▶ TriggerEvent × Pool.Get() [GC Gen0= 0] + 总耗时 : 242.015 ms + 单次 : 0.2420 μs/op + 吞吐量 : 4131982 ops/s ▶ TriggerEvent × 已取消事件跳过(10 订阅者) - 总耗时 : 25.282 ms - 单次 : 0.2528 μs/op - 吞吐量 : 3955352 ops/s + 总耗时 : 52.777 ms + 单次 : 0.5278 μs/op + 吞吐量 : 1894776 ops/s ══════════════════════════════════════════════════ - 全部测试完成 \ No newline at end of file + 全部测试完成 + + 对比 1.1.x 旧样本:new() 路径 7684ms / GC Gen0=6457 → 266ms / GC Gen0=1 + (EventBase 不再每实例分配内置 ListenerList,new() 路径 GC 基本归零, + 对象池从“高频必需”降级为可选优化。) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e2a1b..179bd90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ 本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。 +## [1.3.0] - 2026-06-12 + +### Breaking + +- 移除 `EventBase.GetListenerList()`。事件对象不再内置 `ListenerList`,改为持有派发时的快照数组;调试请使用 `GetSubscribers()`(现在返回拷贝)。 +- 数字优先级 `0` 现在映射到 `NORMAL`(原为 `LOW`),与枚举重载的默认值保持一致。 +- 所有 int 数字优先级重载(`RegisterEvent` / `SubscribeEvent`)不再提供默认值 `0`,必须显式传入数字——消除与枚举重载之间的重载二义性。不带优先级的调用现在唯一解析到枚举重载(`NORMAL`)。 + +### Fixed + +- 织入前会检查类型自身及基类链上是否存在实例 `[EventSubscribe]` 方法,没有的类型不再织入,避免在 `Awake` 中抛出注册异常。 +- `AutoRegister`(含 Mod 框架反射路径)遇到没有任何匹配 `[EventSubscribe]` 方法的类型时,改为输出警告并跳过,不再抛异常;显式 `Register()` 仍保持严格抛错。 +- `ListenerList` 父子链脏标记传播与快照重建之间的竞态:同一总线内所有监听列表(含父子链)现共用一把锁。 +- `RegisterCore` 的 check-then-act 竞态(并发下同一实例可能被注册两次)。 +- `EventPool.Release` 的双重归还竞态;对象池增加容量上限(128),超出直接丢弃。 +- 父子事件同 phase 内的 handler 顺序:现在按数字优先级跨父子稳定归并(平局时子类型 handler 在前),数字优先级不再只在单层内生效。 + +### Changed + +- 派发热路径重构:事件对象的监听者快照从“逐个带锁二分插入内置 ListenerList”改为一次数组引用赋值;`EventId` 改为懒生成;事件元数据缓存改用 `ConcurrentDictionary`。每个事件实例的构造分配显著减少。 +- 删除死代码 `EventCache`(README 此前描述的“泛型静态缓存热路径”自实例化总线改造后已不存在)。 +- 恢复模块内 `CodeGen/` 作为可选本地织入管线:命中 `ShrinkShared.CodeGen` 覆盖范围的程序集继续由共享管线优先处理,其余只引用 `ShrinkEventBus.Runtime` 的程序集改由本地 `EventBusILPostProcessor` 兜底,避免 `[EventBusSubscriber]` 自动注册失效。 +- `GetEventSubscribers` / `GetAllSubscribersSnapshot` / `EventBase.GetSubscribers` 返回内部快照的拷贝,外部修改不再影响派发顺序。 +- `SetPhase` / `IsCanceled` / `Result` 的校验异常现在携带具体错误消息。 +- ILPP 的诊断列表与程序集解析器改为调用内局部状态,消除共享实例并发隐患。 + +## [1.2.0] - 2026-05-18 + +### Changed + +- `MonoBehaviour` 自动注册/反注册的 IL 织入已并入共享 `ShrinkShared.CodeGen` 管线;当前工作区同时保留 `ShrinkEventBus` 本地 `CodeGen/` 作为可选兜底,以兼容纯 `ShrinkEventBus.Runtime` 引用程序集。 +- 保留现有 `[EventBusSubscriber]` 用法与自动生命周期行为,不要求业务层改写订阅代码。 + +## [1.1.6] - 2026-05-04 + +### Changed + +- 运行时总线升级为“默认静态门面 + 可实例化 `IShrinkEventBus`/Builder”双层结构,支持异常策略、事件类型校验、按 phase 分发。 +- 手动注册能力从纯 delegate 扩展为 `object / Type / MethodInfo` 三类入口,并在注册阶段严格校验签名、静态/实例模式和事件参数类型。 +- `ListenerList` 改为按 phase 分桶 + 快照缓存模型,并支持父事件监听子事件。 +- 事件查看器不再反射私有字典,改走公开快照接口。 +- `EventBusBenchmark` 扩展为覆盖实例 bus、扫描注册、继承监听、phase 分发、对象池与取消事件跳过等关键场景。 + ## [1.1.5] - 2026-04-07 ### Changed diff --git a/CodeGen/Editor/EventBusILPostProcessor.cs b/CodeGen/Editor/EventBusILPostProcessor.cs index 0043818..7881503 100644 --- a/CodeGen/Editor/EventBusILPostProcessor.cs +++ b/CodeGen/Editor/EventBusILPostProcessor.cs @@ -1,78 +1,198 @@ +#nullable enable + +using System; using System.Collections.Generic; using System.IO; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; +using Mono.Cecil.Pdb; using Unity.CompilationPipeline.Common.Diagnostics; using Unity.CompilationPipeline.Common.ILPostProcessing; namespace ShrinkEventBus.CodeGen { - public class EventBusILPostProcessor : ILPostProcessor + public sealed class EventBusILPostProcessor : ILPostProcessor { - private readonly List _diagnostics = new(); - private PostProcessorAssemblyResolver _assemblyResolver; - private MethodReference _autoRegisterMethodRef; - private MethodReference _unregisterInstanceMethodRef; + private const string RuntimeAssemblyName = "ShrinkEventBus.Runtime"; + private static readonly string[] SharedCoverageAssemblyNames = + { + "ShrinkCommand.Runtime", + "ShrinkNetwork.Runtime", + "ShrinkApp.Core.Runtime" + }; public override ILPostProcessor GetInstance() => this; public override bool WillProcess(ICompiledAssembly compiledAssembly) { - return compiledAssembly.References.Any( - r => Path.GetFileNameWithoutExtension(r) == "ShrinkEventBus.Runtime"); + if (!ReferencesAssembly(compiledAssembly, RuntimeAssemblyName)) + return false; + + if (ShouldDeferToSharedCodeGen(compiledAssembly)) + return false; + + return true; } public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) { - if (!WillProcess(compiledAssembly)) return null; - _diagnostics.Clear(); + var diagnostics = new List(); + if (!WillProcess(compiledAssembly)) + return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics); var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly); - var mainModule = assemblyDefinition.MainModule; + var module = assemblyDefinition.MainModule; - bool needWeaving = false; - foreach (var type in mainModule.Types) + try { - if (HasAttribute(type, "EventBusSubscriberAttribute") && InheritsFromMonoBehaviour(type)) + var subscriberType = FindType(module, "ShrinkEventBus.EventBusSubscriberAttribute", RuntimeAssemblyName); + var subscribeAttributeType = FindType(module, "ShrinkEventBus.EventSubscribeAttribute", RuntimeAssemblyName); + var eventBusType = FindType(module, "ShrinkEventBus.EventBus", RuntimeAssemblyName); + var staticRegistryCtor = FindTypeArrayConstructor( + module, + "ShrinkEventBus.EventBusStaticRegistryAttribute", + RuntimeAssemblyName); + if (subscriberType == null || subscribeAttributeType == null || eventBusType == null) + return GetResult(assemblyDefinition, diagnostics); + + var eventBusTypeDef = eventBusType.Resolve(); + var autoRegisterMethod = eventBusTypeDef?.Methods.FirstOrDefault(method => + method.Name == "AutoRegister" && + method.IsStatic && + method.Parameters.Count == 1); + var unregisterMethod = eventBusTypeDef?.Methods.FirstOrDefault(method => + method.Name == "UnregisterInstance" && + method.IsStatic && + method.Parameters.Count == 1); + if (autoRegisterMethod == null || unregisterMethod == null) + return GetResult(assemblyDefinition, diagnostics); + + var autoRegisterMethodRef = module.ImportReference(autoRegisterMethod); + var unregisterMethodRef = module.ImportReference(unregisterMethod); + + var subscriberTypes = module.Types + .Where(type => HasAttribute(type, subscriberType)) + .Where(InheritsFromMonoBehaviour) + .Where(type => HasInstanceSubscribeMethod(type, subscribeAttributeType)) + .ToArray(); + + foreach (var type in subscriberTypes) + { + InjectAutoRegister(type, module, autoRegisterMethodRef); + InjectAutoUnregister(type, module, unregisterMethodRef); + } + + if (staticRegistryCtor != null) + { + var staticSubscriberTypes = module.Types + .Where(type => HasAttribute(type, subscriberType)) + .Where(type => type.Methods.Any(method => + method.IsStatic && HasAttribute(method, subscribeAttributeType))) + .Select(type => module.ImportReference(type)) + .ToArray(); + if (staticSubscriberTypes.Length > 0) + AddAssemblyTypeArrayAttribute(module, staticRegistryCtor, staticSubscriberTypes); + } + } + catch (Exception ex) + { + diagnostics.Add(new DiagnosticMessage + { + DiagnosticType = DiagnosticType.Error, + MessageData = $"[ShrinkEventBus.CodeGen] {ex.Message}" + }); + } + + return GetResult(assemblyDefinition, diagnostics); + } + + private static bool ShouldDeferToSharedCodeGen(ICompiledAssembly compiledAssembly) + { + if (!IsSharedCodeGenAvailable()) + return false; + + return SharedCoverageAssemblyNames.Any(name => ReferencesAssembly(compiledAssembly, name)); + } + + private static bool IsSharedCodeGenAvailable() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (string.Equals(assembly.GetName().Name, "Unity.ShrinkShared.CodeGen", StringComparison.Ordinal)) + return true; + } + + return Type.GetType("ShrinkShared.CodeGen.ShrinkRegistryILPostProcessor, Unity.ShrinkShared.CodeGen", false) != null; + } + + private static bool ReferencesAssembly(ICompiledAssembly compiledAssembly, string assemblyName) + { + return compiledAssembly.References.Any(reference => + string.Equals(Path.GetFileNameWithoutExtension(reference), assemblyName, StringComparison.Ordinal)); + } + + private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference expectedAttributeType) + { + return provider.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == expectedAttributeType.FullName); + } + + private static bool HasInstanceSubscribeMethod(TypeDefinition type, TypeReference subscribeAttributeType) + { + var current = type; + while (current != null && current.Name != "MonoBehaviour") + { + if (current.Methods.Any(method => !method.IsStatic && HasAttribute(method, subscribeAttributeType))) + return true; + + var baseTypeRef = current.BaseType; + if (baseTypeRef == null) + return false; + + TypeDefinition? resolvedBase; + try + { + resolvedBase = baseTypeRef.Resolve(); + } + catch + { + return true; + } + + if (resolvedBase == null) + return true; + + current = resolvedBase; + } + + return false; + } + + private static bool InheritsFromMonoBehaviour(TypeDefinition type) + { + var current = type.BaseType; + while (current != null) + { + if (current.Name == "MonoBehaviour") + return true; + + try + { + current = current.Resolve()?.BaseType; + } + catch { - needWeaving = true; break; } } - if (!needWeaving) return null; - - try - { - ImportReferences(mainModule); - } - catch (System.Exception ex) - { - _diagnostics.Add(new DiagnosticMessage - { - DiagnosticType = DiagnosticType.Error, - MessageData = $"[ShrinkEventBus] ILPostProcessor Error: {ex.Message}" - }); - return GetResult(assemblyDefinition, _diagnostics); - } - - foreach (var type in mainModule.Types) - { - if (!HasAttribute(type, "EventBusSubscriberAttribute")) continue; - if (!InheritsFromMonoBehaviour(type)) continue; - - InjectAutoRegister(type, mainModule); - InjectAutoUnregister(type, mainModule); - } - - return GetResult(assemblyDefinition, _diagnostics); + return false; } - private void InjectAutoRegister(TypeDefinition type, ModuleDefinition module) + private static void InjectAutoRegister(TypeDefinition type, ModuleDefinition module, MethodReference autoRegisterMethodRef) { - var awake = type.Methods.FirstOrDefault(m => m.Name == "Awake" && !m.IsStatic); + var awake = type.Methods.FirstOrDefault(method => method.Name == "Awake" && !method.IsStatic); if (awake == null) { var baseAwakeRef = FindBaseMethodReference(type, "Awake", module); @@ -85,7 +205,7 @@ namespace ShrinkEventBus.CodeGen il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, baseAwakeRef); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _autoRegisterMethodRef); + il.Emit(OpCodes.Call, autoRegisterMethodRef); il.Emit(OpCodes.Ret); type.Methods.Add(awake); return; @@ -103,16 +223,16 @@ namespace ShrinkEventBus.CodeGen var instructions = new List { processor.Create(OpCodes.Ldarg_0), - processor.Create(OpCodes.Call, _autoRegisterMethodRef), + processor.Create(OpCodes.Call, autoRegisterMethodRef), processor.Create(OpCodes.Nop) }; instructions.Reverse(); - instructions.ForEach(i => processor.Body.Instructions.Insert(0, i)); + instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); } - private void InjectAutoUnregister(TypeDefinition type, ModuleDefinition module) + private static void InjectAutoUnregister(TypeDefinition type, ModuleDefinition module, MethodReference unregisterMethodRef) { - var onDestroy = type.Methods.FirstOrDefault(m => m.Name == "OnDestroy" && !m.IsStatic); + var onDestroy = type.Methods.FirstOrDefault(method => method.Name == "OnDestroy" && !method.IsStatic); if (onDestroy == null) { var baseOnDestroyRef = FindBaseMethodReference(type, "OnDestroy", module); @@ -123,7 +243,7 @@ namespace ShrinkEventBus.CodeGen module.TypeSystem.Void); var il = onDestroy.Body.GetILProcessor(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _unregisterInstanceMethodRef); + il.Emit(OpCodes.Call, unregisterMethodRef); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, baseOnDestroyRef); il.Emit(OpCodes.Ret); @@ -143,14 +263,14 @@ namespace ShrinkEventBus.CodeGen var instructions = new List { processor.Create(OpCodes.Ldarg_0), - processor.Create(OpCodes.Call, _unregisterInstanceMethodRef), + processor.Create(OpCodes.Call, unregisterMethodRef), processor.Create(OpCodes.Nop) }; instructions.Reverse(); - instructions.ForEach(i => processor.Body.Instructions.Insert(0, i)); + instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); } - private static MethodReference FindBaseMethodReference(TypeDefinition type, string methodName, ModuleDefinition module) + private static MethodReference? FindBaseMethodReference(TypeDefinition type, string methodName, ModuleDefinition module) { try { @@ -158,11 +278,11 @@ namespace ShrinkEventBus.CodeGen while (baseTypeRef != null) { var baseTypeDef = baseTypeRef.Resolve(); - if (baseTypeDef == null) break; - - var method = baseTypeDef.Methods.FirstOrDefault( - m => m.Name == methodName && !m.IsStatic && m.IsVirtual); + if (baseTypeDef == null) + break; + var method = baseTypeDef.Methods.FirstOrDefault(candidate => + candidate.Name == methodName && !candidate.IsStatic && candidate.IsVirtual); if (method != null) { if (baseTypeRef is GenericInstanceType genericInstance) @@ -187,106 +307,69 @@ namespace ShrinkEventBus.CodeGen } catch { - // ignored } return null; } - private void ImportReferences(ModuleDefinition module) + private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName) { - var runtimeModule = FindRuntimeModule(module); - if (runtimeModule == null) - { - throw new System.Exception("Cannot find ShrinkEventBus.Runtime module in assembly references."); - } - - var eventBusType = runtimeModule.GetAllTypes().FirstOrDefault(t => t.Name == "EventBus"); - if (eventBusType == null) - { - throw new System.Exception("Cannot find EventBus class in ShrinkEventBus.Runtime."); - } - - _autoRegisterMethodRef = module.ImportReference( - eventBusType.Methods.First(m => m.Name == "AutoRegister")); - _unregisterInstanceMethodRef = module.ImportReference( - eventBusType.Methods.First(m => m.Name == "UnregisterInstance")); + var resolved = Type.GetType($"{fullName}, {assemblyName}", false); + return resolved == null ? null : module.ImportReference(resolved); } - private ModuleDefinition FindRuntimeModule(ModuleDefinition module) + private static MethodReference? FindTypeArrayConstructor(ModuleDefinition module, string fullName, string assemblyName) { - ModuleDefinition result = null; - var visited = new HashSet(); - SearchRecursive(_assemblyResolver.Resolve(module.Assembly.Name), ref result, visited); - return result; + var typeRef = FindType(module, fullName, assemblyName); + var typeDef = typeRef?.Resolve(); + var ctor = typeDef?.Methods.FirstOrDefault(method => + method.IsConstructor && + method.Parameters.Count == 1 && + method.Parameters[0].ParameterType.IsArray && + method.Parameters[0].ParameterType.GetElementType().FullName == module.ImportReference(typeof(Type)).FullName); + return ctor == null ? null : module.ImportReference(ctor); } - private void SearchRecursive(AssemblyDefinition asm, ref ModuleDefinition found, HashSet visited) + private static void AddAssemblyTypeArrayAttribute(ModuleDefinition module, MethodReference ctor, TypeReference[] types) { - if (asm == null) return; - - foreach (var mod in asm.Modules) - { - if (mod.Name == "ShrinkEventBus.Runtime.dll" || mod.Name == "ShrinkEventBus.Runtime") - { - found = mod; - return; - } - } - foreach (var reference in asm.MainModule.AssemblyReferences) - { - if (visited.Contains(reference.Name)) continue; - visited.Add(reference.Name); - var resolved = _assemblyResolver.Resolve(reference); - if (resolved == null) continue; - SearchRecursive(resolved, ref found, visited); - if (found != null) return; - } + var attribute = new CustomAttribute(ctor); + var typeTypeRef = module.ImportReference(typeof(Type)); + attribute.ConstructorArguments.Add(new CustomAttributeArgument( + module.ImportReference(typeof(Type[])), + types.Select(type => new CustomAttributeArgument(typeTypeRef, type)).ToArray())); + module.Assembly.CustomAttributes.Add(attribute); } - private AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly) + private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly) { - _assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly); + var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly); var readerParameters = new ReaderParameters { SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()), - SymbolReaderProvider = new PortablePdbReaderProvider(), - AssemblyResolver = _assemblyResolver, + SymbolReaderProvider = new PdbReaderProvider(), + AssemblyResolver = assemblyResolver, ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(), ReadingMode = ReadingMode.Immediate }; - var peStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()); - var assemblyDef = AssemblyDefinition.ReadAssembly(peStream, readerParameters); - _assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDef); - return assemblyDef; + + var assemblyDefinition = AssemblyDefinition.ReadAssembly( + new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()), + readerParameters); + assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition); + return assemblyDefinition; } - private static ILPostProcessResult GetResult(AssemblyDefinition asm, List diagnostics) + private static ILPostProcessResult GetResult(AssemblyDefinition assemblyDefinition, List diagnostics) { var pe = new MemoryStream(); var pdb = new MemoryStream(); - asm.Write(pe, new WriterParameters + assemblyDefinition.Write(pe, new WriterParameters { - SymbolWriterProvider = new PortablePdbWriterProvider(), + SymbolWriterProvider = new PdbWriterProvider(), SymbolStream = pdb, WriteSymbols = true }); return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics); } - - private static bool HasAttribute(TypeDefinition type, string name) - => type.CustomAttributes.Any(a => a.AttributeType.Name == name); - - private static bool InheritsFromMonoBehaviour(TypeDefinition type) - { - var current = type.BaseType; - while (current != null) - { - if (current.Name == "MonoBehaviour") return true; - try { current = current.Resolve()?.BaseType; } - catch { break; } - } - return false; - } } -} \ No newline at end of file +} diff --git a/CodeGen/Editor/PostProcessorAssemblyResolver.cs b/CodeGen/Editor/PostProcessorAssemblyResolver.cs index 3bc86a9..38c7606 100644 --- a/CodeGen/Editor/PostProcessorAssemblyResolver.cs +++ b/CodeGen/Editor/PostProcessorAssemblyResolver.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +#nullable enable + +using System.Collections.Generic; using System.IO; using System.Threading; using Mono.Cecil; @@ -6,11 +8,11 @@ using Unity.CompilationPipeline.Common.ILPostProcessing; namespace ShrinkEventBus.CodeGen { - internal class PostProcessorAssemblyResolver : IAssemblyResolver + internal sealed class PostProcessorAssemblyResolver : IAssemblyResolver { private readonly string[] _references; private readonly Dictionary _cache = new(); - private AssemblyDefinition _self; + private AssemblyDefinition? _self; public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly) { @@ -22,10 +24,12 @@ namespace ShrinkEventBus.CodeGen _self = assemblyDefinition; } - public AssemblyDefinition Resolve(AssemblyNameReference name) - => Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + public AssemblyDefinition? Resolve(AssemblyNameReference name) + { + return Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + } - public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters) { lock (_cache) { @@ -33,10 +37,12 @@ namespace ShrinkEventBus.CodeGen return _self; var path = FindPath(name); - if (path == null) return null; + if (path == null) + return null; var key = $"{path}{File.GetLastWriteTime(path)}"; - if (_cache.TryGetValue(key, out var cached)) return cached; + if (_cache.TryGetValue(key, out var cached)) + return cached; parameters.AssemblyResolver = this; var ms = ReadFileWithRetry(path); @@ -51,25 +57,27 @@ namespace ShrinkEventBus.CodeGen } } - private string FindPath(AssemblyNameReference name) + private string? FindPath(AssemblyNameReference name) { - foreach (var r in _references) + foreach (var reference in _references) { - if (Path.GetFileNameWithoutExtension(r) == name.Name) - return r; + if (Path.GetFileNameWithoutExtension(reference) == name.Name) + return reference; } var dirs = new HashSet(); - foreach (var r in _references) + foreach (var reference in _references) { - var dir = Path.GetDirectoryName(r); - if (dir != null) dirs.Add(dir); + var dir = Path.GetDirectoryName(reference); + if (dir != null) + dirs.Add(dir); } foreach (var dir in dirs) { var candidate = Path.Combine(dir, $"{name.Name}.dll"); - if (File.Exists(candidate)) return candidate; + if (File.Exists(candidate)) + return candidate; } return null; @@ -81,17 +89,19 @@ namespace ShrinkEventBus.CodeGen { try { - var bytes = File.ReadAllBytes(path); - return new MemoryStream(bytes); + return new MemoryStream(File.ReadAllBytes(path)); } catch (IOException) when (i < retries - 1) { Thread.Sleep(100); } } - throw new IOException($"[ShrinkEventBus] 无法读取文件: {path}"); + + throw new IOException($"[ShrinkEventBus.CodeGen] 无法读取文件: {path}"); } - public void Dispose() { } + public void Dispose() + { + } } -} \ No newline at end of file +} diff --git a/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta b/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta index c577e75..5aedb8f 100644 --- a/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta +++ b/CodeGen/Editor/PostProcessorAssemblyResolver.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: aeea50f82bab4ee2833c671d801c95e3 -timeCreated: 1772908894 \ No newline at end of file +timeCreated: 1772908894 diff --git a/CodeGen/Editor/PostProcessorReflectionImporter.cs b/CodeGen/Editor/PostProcessorReflectionImporter.cs index 5b4db06..957a46d 100644 --- a/CodeGen/Editor/PostProcessorReflectionImporter.cs +++ b/CodeGen/Editor/PostProcessorReflectionImporter.cs @@ -1,18 +1,21 @@ -using System.Linq; +#nullable enable + +using System.Linq; using System.Reflection; using Mono.Cecil; namespace ShrinkEventBus.CodeGen { - internal class PostProcessorReflectionImporter : DefaultReflectionImporter + internal sealed class PostProcessorReflectionImporter : DefaultReflectionImporter { private const string CoreLibName = "System.Private.CoreLib"; - private readonly AssemblyNameReference _corlib; + private readonly AssemblyNameReference? _corlib; - public PostProcessorReflectionImporter(ModuleDefinition module) : base(module) + public PostProcessorReflectionImporter(ModuleDefinition module) + : base(module) { _corlib = module.AssemblyReferences.FirstOrDefault( - a => a.Name is "mscorlib" or "netstandard"); + assembly => assembly.Name is "mscorlib" or "netstandard"); } public override AssemblyNameReference ImportReference(AssemblyName reference) @@ -24,9 +27,11 @@ namespace ShrinkEventBus.CodeGen } } - internal class PostProcessorReflectionImporterProvider : IReflectionImporterProvider + internal sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider { public IReflectionImporter GetReflectionImporter(ModuleDefinition module) - => new PostProcessorReflectionImporter(module); + { + return new PostProcessorReflectionImporter(module); + } } -} \ No newline at end of file +} diff --git a/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta b/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta index b1f2737..70b8deb 100644 --- a/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta +++ b/CodeGen/Editor/PostProcessorReflectionImporter.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 0a6c2401798b42f0bbcde72796d2fa9c -timeCreated: 1772908906 \ No newline at end of file +timeCreated: 1772908906 diff --git a/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef index f8f2955..24bec64 100644 --- a/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef +++ b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef @@ -20,4 +20,4 @@ "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Editor/EventBusViewerWindow.cs b/Editor/EventBusViewerWindow.cs index be94529..ea0d260 100644 --- a/Editor/EventBusViewerWindow.cs +++ b/Editor/EventBusViewerWindow.cs @@ -64,7 +64,10 @@ namespace ShrinkEventBus.Editor private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All; private bool _collapseSameEventType = true; private const int MaxLogCount = 100; + private const double LiveTrackerRefreshIntervalSeconds = 0.1d; private readonly List _logRecords = new(); + private bool _isLiveTrackerDirty; + private double _nextLiveTrackerRefreshTime; [MenuItem("ShrinkSDK/事件总线/事件查看器")] public static void ShowWindow() @@ -74,8 +77,17 @@ namespace ShrinkEventBus.Editor window.Show(); } - private void OnEnable() => EventBus.OnEventTriggeredForEditor += RecordEventLog; - private void OnDisable() => EventBus.OnEventTriggeredForEditor -= RecordEventLog; + private void OnEnable() + { + EventBus.OnEventTriggeredForEditor += RecordEventLog; + EditorApplication.update += OnEditorUpdate; + } + + private void OnDisable() + { + EventBus.OnEventTriggeredForEditor -= RecordEventLog; + EditorApplication.update -= OnEditorUpdate; + } private void CreateGUI() { @@ -166,7 +178,7 @@ namespace ShrinkEventBus.Editor searchField.RegisterValueChangedCallback(evt => { _liveLogSearchString = evt.newValue ?? string.Empty; - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); }); toolbar.Add(searchField); @@ -175,19 +187,19 @@ namespace ShrinkEventBus.Editor { _liveLogFilterMode = LiveLogFilterMode.All; filterMenu.text = "全部事件"; - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); }); filterMenu.menu.AppendAction("仅有监听者", _ => { _liveLogFilterMode = LiveLogFilterMode.HasListeners; filterMenu.text = "仅有监听者"; - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); }); filterMenu.menu.AppendAction("仅无监听者", _ => { _liveLogFilterMode = LiveLogFilterMode.NoListeners; filterMenu.text = "仅无监听者"; - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); }); toolbar.Add(filterMenu); @@ -199,7 +211,7 @@ namespace ShrinkEventBus.Editor collapseToggle.RegisterValueChangedCallback(evt => { _collapseSameEventType = evt.newValue; - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); }); toolbar.Add(collapseToggle); toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" }); @@ -220,7 +232,7 @@ namespace ShrinkEventBus.Editor _liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } }; container.Add(_liveTrackerScrollView); - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); return container; } @@ -228,7 +240,31 @@ namespace ShrinkEventBus.Editor { _subscribersTab.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None; _liveTrackerTab.style.display = index == 1 ? DisplayStyle.Flex : DisplayStyle.None; - if (index == 0) RefreshSubscribersView(); + if (index == 0) + { + RefreshSubscribersView(); + return; + } + RequestLiveTrackerRefresh(true); + } + + private void OnEditorUpdate() + { + if (!_isLiveTrackerDirty) return; + if (_liveTrackerTab == null || _liveTrackerTab.style.display != DisplayStyle.Flex) return; + if (EditorApplication.timeSinceStartup < _nextLiveTrackerRefreshTime) return; + RefreshLiveTrackerView(); + } + + private void RequestLiveTrackerRefresh(bool immediate = false) + { + _isLiveTrackerDirty = true; + if (immediate) + { + _nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup; + if (_liveTrackerTab != null && _liveTrackerTab.style.display == DisplayStyle.Flex) + RefreshLiveTrackerView(); + } } private void RefreshSubscribersView() @@ -240,20 +276,14 @@ namespace ShrinkEventBus.Editor return; } - var field = typeof(EventBus).GetField("EventHandlers", BindingFlags.NonPublic | BindingFlags.Static); - if (field == null) return; - var dict = field.GetValue(null) as IDictionary; - if (dict == null) return; - - foreach (DictionaryEntry entry in dict) + var snapshots = EventBus.GetAllSubscribersSnapshot(); + foreach (var entry in snapshots) { - var eventType = entry.Key as Type; + var eventType = entry.Key; if (!string.IsNullOrEmpty(_searchString) && !eventType.Name.ToLower().Contains(_searchString.ToLower())) continue; - var listenerListObj = entry.Value; - var handlers = - listenerListObj.GetType().GetMethod("GetHandlers")?.Invoke(listenerListObj, null) as Array; + var handlers = entry.Value; var foldout = new Foldout { @@ -273,20 +303,19 @@ namespace ShrinkEventBus.Editor } } - private void RecordEventLog(EventBase evt, string eventName, string senderInfo) + private void RecordEventLog(EventBase evt, string eventName, string senderInfo, EventHandlerInfo[] handlers) { if (!EventBus.EnableDebugRecord) return; - _logRecords.Insert(0, CreateEventLogRecord(evt, eventName, senderInfo)); + _logRecords.Add(CreateEventLogRecord(evt, eventName, senderInfo, handlers)); if (_logRecords.Count > MaxLogCount) - _logRecords.RemoveAt(_logRecords.Count - 1); - RefreshLiveTrackerView(); + _logRecords.RemoveAt(0); + RequestLiveTrackerRefresh(); } - private EventLogRecord CreateEventLogRecord(EventBase evt, string eventName, string senderInfo) + private EventLogRecord CreateEventLogRecord(EventBase evt, string eventName, string senderInfo, + EventHandlerInfo[] handlers) { var timestamp = DateTime.Now; - var list = evt.GetListenerList(); - var handlers = list?.GetHandlers() ?? Array.Empty(); var hasListeners = handlers.Length > 0; var paramsList = DumpEventParams(evt); var handlerRows = new List(handlers.Length); @@ -325,12 +354,15 @@ namespace ShrinkEventBus.Editor private void RefreshLiveTrackerView() { if (_liveTrackerScrollView == null) return; + _isLiveTrackerDirty = false; + _nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup + LiveTrackerRefreshIntervalSeconds; _liveTrackerScrollView.Clear(); - var filteredRecords = new List(); - foreach (var record in _logRecords) + var filteredRecords = new List(_logRecords.Count); + for (var i = _logRecords.Count - 1; i >= 0; i--) { + var record = _logRecords[i]; if (MatchesLiveLogFilter(record)) filteredRecords.Add(record); } @@ -425,15 +457,36 @@ namespace ShrinkEventBus.Editor } }; foldout.Add(summary); + if (foldout.value) + PopulateCollapsedGroupDetails(foldout, group, 1); + foldout.RegisterValueChangedCallback(evt => + { + if (evt.newValue) + { + PopulateCollapsedGroupDetails(foldout, group, 1); + return; + } + ClearFoldoutChildren(foldout, 1); + }); + return foldout; + } + + private void PopulateCollapsedGroupDetails(Foldout foldout, EventLogGroup group, int keepChildCount) + { + if (foldout.childCount > keepChildCount) return; foreach (var record in group.Records) { var row = CreateLogEntryElement(record); row.style.marginLeft = 12; foldout.Add(row); } + } - return foldout; + private void ClearFoldoutChildren(Foldout foldout, int keepChildCount) + { + while (foldout.childCount > keepChildCount) + foldout.RemoveAt(foldout.childCount - 1); } private VisualElement CreateLogEntryElement(EventLogRecord record) @@ -714,7 +767,7 @@ namespace ShrinkEventBus.Editor private void ClearLogs() { _logRecords.Clear(); - RefreshLiveTrackerView(); + RequestLiveTrackerRefresh(true); } private void OpenScriptByClassName(string className) diff --git a/README.md b/README.md index eb2063a..e51dc7b 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,34 @@ | 特性 | 说明 | |------|------| | 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 | -| ⚡ **高性能热路径** | 泛型静态缓存 `EventCache` 绕过字典查找,触发路径几乎零开销 | +| ⚡ **高性能热路径** | 注册期预编译 invoker(无反射调用、无装箱),派发走缓存快照数组,监听者快照零拷贝挂接 | | 🤖 **零侵入自动注册** | 标记 `[EventBusSubscriber]` 即可,ILPostProcessor 编译期自动织入注册与反注册逻辑,动态创建的对象也无需手写任何代码 | +| 🧾 **静态订阅清单** | 静态 `[EventBusSubscriber]` 现可通过编译期注册表收口,避免默认总线启动时全域扫描所有类型 | | 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 | -| 🔄 **同步 & 异步** | 统一支持 `Action`、`UniTask`、`Task` 三种 handler 形式 | +| 🔄 **同步 & 异步** | 统一支持 `Action`(同步)与 `UniTask`(异步)两种 handler 形式 | | 🧵 **线程安全** | 注册/注销操作全程加锁保护 | | 📦 **对象池** | 内置 `EventPool`,高频事件零 GC | | 🔍 **调试友好** | Editor 事件查看器实时追踪订阅者与触发日志 | +| 🧩 **可实例化总线** | 除默认静态 `EventBus` 外,也可以用 Builder 创建独立 bus,并按需要配置异常策略、事件类型约束、分 phase 分发 | +| 🌳 **父事件监听** | 监听父事件类型时,子事件触发也会命中父事件监听器,便于做 Pre/Post 家族事件和统一监控 | ## 👓 Benchmark [Benchmark结果](Benchmark.txt) +运行时仓库里自带 `EventBusBenchmark` 组件,当前会分别覆盖这些场景: + +- 无订阅者 / 单订阅者 / 多订阅者同步触发 +- 父事件监听子事件 +- 按 `EventPriority` 分 phase 分发 +- 单订阅者异步触发 +- 手工 delegate 注册 / 注销 +- `object / Type / MethodInfo` 扫描注册 / 注销 +- `EventPool` 与 `new` +- 已取消事件跳过 + +如果你在评估这次 `IShrinkEventBus`、继承监听和严格注册带来的成本变化,优先看这个组件的输出,而不是只看 `Benchmark.txt` 里的旧样本。 + ## 📦 依赖 - Unity 2022.3+ @@ -43,6 +59,8 @@ https://github.com/cneicy/ShrinkEventBus.git ``` +> ⚠️ **自动织入依赖说明**:当前工作区同时支持两层织入。若当前程序集命中共享管线 `ShrinkShared.CodeGen` 的覆盖范围(例如同时引用 `ShrinkCommand.Runtime` / `ShrinkNetwork.Runtime` / `ShrinkApp.Core.Runtime`),则优先由共享管线处理;其余只引用 `ShrinkEventBus.Runtime` 的程序集由模块内 `CodeGen/` 本地 ILPostProcessor 兜底。独立安装本包但未带上 `CodeGen/` 或共享管线时,没有编译期织入,请改用手动接入:MonoBehaviour 在 `Awake`/`OnDestroy` 中调用 `EventBus.AutoRegister(this)` / `EventBus.UnregisterInstance(this)`,或使用 `SubscribeEvent` 句柄。 + ## 🚀 快速上手 ### 第一步:定义事件 @@ -136,7 +154,13 @@ EventBus.TriggerEvent(evt); ### 自动注册机制 -ShrinkEventBus 通过 **ILPostProcessor** 在编译期自动处理完整的生命周期管理。当 Unity 编译代码时,所有标记了 `[EventBusSubscriber]` 的 MonoBehaviour 子类会被自动识别,并在其 `Awake` 和 `OnDestroy` 方法中分别织入注册与反注册逻辑。 +ShrinkEventBus 通过 **ILPostProcessor** 在编译期自动处理完整的生命周期管理。当 Unity 编译代码时,所有标记了 `[EventBusSubscriber]` 且自身或基类链上存在实例 `[EventSubscribe]` 方法的 MonoBehaviour 子类会被自动识别,并在其 `Awake` 和 `OnDestroy` 方法中分别织入注册与反注册逻辑(没有任何实例订阅方法的类型会被跳过,不织入也不报错)。 + +当前织入策略如下: + +- 命中共享管线 `ShrinkShared.CodeGen` 覆盖范围的程序集,优先由共享管线处理。 +- 其余只引用 `ShrinkEventBus.Runtime` 的程序集,由模块内 `CodeGen/Editor/EventBusILPostProcessor.cs` 本地处理。 +- 因此 ShrinkSDK 工作区内的统一 CodeGen 与独立 `ShrinkEventBus` 业务程序集可以同时兼容,且不会双重织入。 织入规则如下: @@ -152,6 +176,57 @@ ShrinkEventBus 通过 **ILPostProcessor** 在编译期自动处理完整的生 **整个过程对业务代码完全透明,类里不需要写任何注册相关的代码。** +### 实例化总线与 Builder + +默认情况下,项目继续使用全局静态门面 `EventBus`。如果你需要更清晰的模块边界,也可以创建独立 bus: + +```csharp +var gameplayBus = EventBus.CreateBus(builder => builder + .AllowPerPhaseDispatch() + .SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndThrow)); + +gameplayBus.Register(new GameplaySubscribers()); +gameplayBus.TriggerEvent(new PlayerDiedEvent { PlayerId = 1, Cause = "Fall" }); +``` + +当前 Builder 支持的重点配置: + +- `SetExceptionHandlingMode(...)` +- `SetExceptionHandler(...)` +- `AllowPerPhaseDispatch()` +- `CheckTypesOnDispatch()` +- `MarkerInterface()` +- `ClassChecker(...)` +- `StartShutdown()` + +如果你只是想继续沿用旧习惯,直接用静态 `EventBus` 即可;它内部就是一个默认的 `IShrinkEventBus` 实例。 + +### 手动注册的新边界 + +除了 `AutoRegister(this)` / `[EventBusSubscriber]` 这条 Unity 友好的自动接入路径,现在也支持更显式的手动注册: + +```csharp +// 扫描实例上的 [EventSubscribe] 方法 +EventBus.Register(mySubscriberInstance); + +// 扫描某个类型上的 static [EventSubscribe] 方法 +EventBus.Register(typeof(GlobalEventHooks)); + +// 只注册某一个 static [EventSubscribe] 方法 +EventBus.Register(typeof(GlobalEventHooks).GetMethod("OnPlayerDied", + BindingFlags.Static | BindingFlags.NonPublic)); +``` + +和旧版本相比,手动注册现在会更严格: + +- 方法必须带 `[EventSubscribe]` +- 只能有一个参数 +- 参数必须继承 `EventBase` +- 返回值只能是 `void` 或 `UniTask` +- 实例注册只接受实例方法,类型/方法注册只接受静态方法 + +这样做的目的是把“为什么没触发”尽量提前到注册阶段暴露,而不是静默吞掉。 + ### 优先级系统 `EventPriority` 枚举定义了六个优先级档位,数值越小越先执行: @@ -167,7 +242,7 @@ HIGHEST(0) → HIGH(1) → NORMAL(2) → LOW(3) → LOWEST(4) → MONITOR(5) [EventSubscribe(EventPriority.HIGH)] private void Handler(SomeEvent evt) { } -// 数字优先级(自动映射到枚举档位) +// 数字优先级(自动映射到枚举档位;手动注册时必须显式传入数字) EventBus.RegisterEvent(Handler, priority: 75); // 映射为 HIGH // 手动注册时混合使用 @@ -180,10 +255,12 @@ EventBus.RegisterEvent(Handler, EventPriority.HIGH, receiveCanceled: |---------|---------| | ≥ 100 | HIGHEST | | ≥ 50 | HIGH | -| > 0 | NORMAL | +| ≥ 0 | NORMAL | | ≥ -50 | LOW | | < -50 | LOWEST | +> 1.3.0 起:数字 `0` 映射到 `NORMAL`(与枚举重载默认值一致);int 重载不再提供默认值,不带优先级的 `RegisterEvent(handler)` 调用唯一解析到枚举重载(NORMAL)。 + **推荐的优先级分工:** ``` @@ -195,6 +272,36 @@ LOWEST — 收尾清理 MONITOR — 日志、统计、监控(通常配合 receiveCanceled: true) ``` +如果你创建的 bus 开启了 `AllowPerPhaseDispatch()`,也可以只分发某一个 phase: + +```csharp +gameplayBus.TriggerEvent(EventPriority.HIGH, evt); +await gameplayBus.TriggerEventAsync(EventPriority.MONITOR, evt); +``` + +这个模式主要适合做框架级流水线控制;普通业务仍推荐直接走完整分发。 + +### 父事件监听 + +现在监听父事件时,子事件触发也会命中父事件监听器: + +```csharp +public class DamageEvent : EventBase +{ + public int Value { get; set; } +} + +public sealed class CriticalDamageEvent : DamageEvent +{ + public bool IsCritical { get; set; } +} + +EventBus.RegisterEvent(OnAnyDamage, EventPriority.MONITOR, receiveCanceled: true); +EventBus.TriggerEvent(new CriticalDamageEvent { Value = 42, IsCritical = true }); +``` + +这很适合做统一日志、统一权限检查、事件族级别的监控和桥接。 + ### 事件取消与结果 ```csharp @@ -244,7 +351,8 @@ bool success = pickupEvent.Result switch | 方式 | 适用场景 | 自动反注册 | |------|---------|-----------| | `[EventBusSubscriber]` + `[EventSubscribe]` | MonoBehaviour(推荐) | ✅ ILP 织入 OnDestroy,随 GameObject 销毁自动清理 | -| `EventBus.RegisterEvent(...)` 手动注册 | 非 MonoBehaviour 类、Lambda | ❌ 需手动调用 `UnregisterEvent` | +| `EventBus.SubscribeEvent(...)` 手动订阅 | 非 MonoBehaviour 类、Lambda | ✅ `Dispose()` 即可精准清理 | +| `EventBus.RegisterEvent(...)` 手动注册 | 兼容旧代码 | ❌ 需手动调用 `UnregisterEvent` | | `EventBus.AutoRegister(this)` | 特殊场景下手动触发 | ❌ 需手动调用 `UnregisterInstance` | **手动注册示例(非 MonoBehaviour):** @@ -252,17 +360,18 @@ bool success = pickupEvent.Result switch ```csharp public class InventorySystem : IDisposable { + private readonly IShrinkEventSubscription _itemPickupSubscription; + public InventorySystem() { - EventBus.RegisterEvent(OnItemPickup, EventPriority.NORMAL); + _itemPickupSubscription = EventBus.SubscribeEvent(OnItemPickup, EventPriority.NORMAL); } private void OnItemPickup(ItemPickupEvent evt) { /* ... */ } public void Dispose() { - // 必须手动清理,否则 handler 持有 this 引用会造成内存泄漏 - EventBus.UnregisterAllEventsForObject(this); + _itemPickupSubscription.Dispose(); } } ``` @@ -279,9 +388,12 @@ public class InventorySystem : IDisposable // 同步 handler EventBus.RegisterEvent(Action handler, EventPriority priority, bool receiveCanceled); EventBus.RegisterEvent(Action handler, int priority); +EventBus.SubscribeEvent(Action handler, EventPriority priority, bool receiveCanceled); +EventBus.SubscribeEvent(Action handler, int priority); // 异步 handler(UniTask) EventBus.RegisterEvent(Func handler, EventPriority priority, bool receiveCanceled); +EventBus.SubscribeEvent(Func handler, EventPriority priority, bool receiveCanceled); // 注销 EventBus.UnregisterEvent(Action handler); @@ -311,6 +423,7 @@ EventBus.GetRegisteredInstanceCount(); EventBus.GetRegisteredEventTypeCount(); EventBus.GetEventSubscribers(); // 返回 EventHandlerInfo[] EventBus.GetListenerList(); // 无订阅者时返回 null +EventBus.GetActiveSubscriptionsSnapshot();// 返回 IDisposable 订阅快照 ``` ### EventPool\ @@ -333,7 +446,7 @@ EventBus.TriggerEvent(evt); ### EventBase 关键成员 ```csharp -evt.EventId // Guid,每次触发唯一 +evt.EventId // Guid,每次派发唯一(懒生成,首次访问时分配) evt.EventTime // 事件创建时间(UTC) evt.IsCancelable // 是否支持取消(由 [Cancelable] 决定) evt.HasResult // 是否支持结果(由 [HasResult] 决定) @@ -341,7 +454,7 @@ evt.IsCanceled // 是否已被取消 evt.Result // 当前结果(EventResult 枚举) evt.Phase // 当前执行到的优先级阶段 evt.CurrentHandler // 当前正在执行的 handler 信息 -evt.GetSubscribers() // 获取 handler 列表快照(调试用) +evt.GetSubscribers() // 获取本次派发的 handler 快照拷贝(调试用) ``` --- @@ -351,46 +464,51 @@ evt.GetSubscribers() // 获取 handler 列表快照(调试用) ``` ShrinkEventBus ├── Runtime/ -│ ├── EventBus 静态门面,所有公开 API 的入口 -│ ├── EventCache 泛型静态缓存,热路径绕过字典查找 -│ ├── ListenerList 线程安全的有序 handler 列表 -│ ├── EventHandlerInfo 单个 handler 的元信息(优先级、方法反射、调试信息) -│ ├── EventBase 所有事件的基类,携带生命周期状态 +│ ├── EventBus 静态门面,内部是一个默认 IShrinkEventBus 实例 +│ ├── ShrinkEventBusInstance 总线实现:注册、派发、异常策略、phase 分发 +│ ├── ShrinkEventBusBuilder 实例总线的构建与配置入口 +│ ├── ListenerList 按 phase 分桶的有序 handler 列表,带快照缓存与父链合并 +│ ├── EventHandlerInfo 单个 handler 的元信息(优先级、预编译 invoker、调试信息) +│ ├── EventBase 所有事件的基类,携带生命周期状态与派发快照 │ ├── EventPool 对象池,高频事件减少 GC +│ ├── EventCloneUtility 同步路径上 async handler 的事件快照克隆 │ ├── EventBusRegHelper 反射扫描 & handler 注册逻辑 │ └── EventAutoRegHelper 运行时初始化,确保 IsInitialized 状态正确 │ ├── Editor/ │ └── EventBusViewerWindow 事件查看器,实时显示订阅者与触发日志 │ -└── CodeGen/ - └── EventBusILPostProcessor 编译期织入,向 [EventBusSubscriber] 类注入 - Awake(AutoRegister)与 OnDestroy(UnregisterInstance) +└── (织入)ShrinkShared.CodeGen / CodeGen 共享 ILPostProcessor 优先,本地 ILPostProcessor 兜底 + 向 [EventBusSubscriber] 类注入 Awake(AutoRegister) + 与 OnDestroy(UnregisterInstance) ``` **热路径(`TriggerEvent`)工作流:** ``` TriggerEvent(evt) - └─ 读取 EventCache.List // 静态字段,O(1),无字典查找 - └─ GetHandlers() // 返回内部快照数组,无拷贝 + └─ 取该事件类型的 ListenerList // 总线级字典 + 共享锁,每类型常数开销 + └─ GetHandlers() // 返回缓存快照数组(脏时才重建),无拷贝 + ├─ 快照数组引用挂到事件对象上(一次赋值,供 GetSubscribers 调试) └─ 遍历 handlers[] ├─ 跳过已取消 & 不接收取消的 handler - ├─ Action → 直接调用 - └─ Func → 基于事件快照 .Forget()(同步路径) + ├─ Action → 经预编译 invoker 直接调用 + └─ Func → 克隆事件快照后 .Forget()(同步路径) ``` **自动注册完整流程:** ``` -【编译期】ILPostProcessor 扫描所有程序集 - └─ 找到标记了 [EventBusSubscriber] 的 MonoBehaviour 子类 +【编译期】若当前程序集命中 ShrinkShared.CodeGen 覆盖范围,则由共享 ILPostProcessor 扫描; + 否则由 CodeGen/EventBusILPostProcessor.cs 本地扫描 + └─ 找到标记了 [EventBusSubscriber] 且存在实例 [EventSubscribe] 方法的 MonoBehaviour 子类 ├─ 在 Awake 头部织入 EventBus.AutoRegister(this) └─ 在 OnDestroy 头部织入 EventBus.UnregisterInstance(this) (类无对应方法时自动生成,有虚基类方法时自动调用 base) + ※ 只引用 ShrinkEventBus.Runtime 的纯业务程序集目前不在织入范围内,需手动 AutoRegister -【运行时 - 场景加载】RuntimeInitializeOnLoadMethod(AfterSceneLoad) - └─ 扫描程序集中的 [EventBusSubscriber] 类型,标记 IsInitialized +【运行时 - 默认静态总线启动】 + └─ 读取编译期静态订阅清单,注册 static [EventSubscribe] 方法 【运行时 - 动态创建】Instantiate(prefab) └─ Unity 调用新对象的 Awake(已含织入代码)→ 自动注册 @@ -460,6 +578,8 @@ public void Dispose() - **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 快照,会产生语义上的不确定性。 - **静态 handler 永远不会自动注销**:静态方法注册后持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。 - **`[EventBusSubscriber]` 仅对 MonoBehaviour 生效自动注册**:非 MonoBehaviour 类标记该 Attribute 无任何效果,请使用手动注册。 +- **标了 `[EventBusSubscriber]` 但没有实例 `[EventSubscribe]` 方法的类**:编译期不会织入;若通过 `AutoRegister` 手动接入,会输出警告并跳过(不抛异常)。显式 `Register()` 对此仍严格抛错。 +- **int 数字优先级重载必须显式传值**:1.3.0 起 int 重载不再有默认值;数字 `0` 映射 `NORMAL`。 - **ILPostProcessor 织入发生在编译期**:修改代码后需要重新编译才能使注入生效,热重载场景下请注意这一点。 - **继承泛型基类(如 `Singleton`)时无需额外处理**:ILP 会正确识别泛型基类中的虚方法并生成 `protected override`,自动调用 `base.Awake()` 和 `base.OnDestroy()`。 @@ -470,7 +590,7 @@ public void Dispose() **事件没有被任何 handler 接收** 1. 检查订阅类是否有 `[EventBusSubscriber]` -2. 检查方法是否有 `[EventSubscribe]`,且签名为 `void/UniTask/Task Method(TEvent evt)` +2. 检查方法是否有 `[EventSubscribe]`,且签名为 `void/UniTask Method(TEvent evt)` 3. 确认代码在标记 `[EventBusSubscriber]` 后重新编译过(ILPostProcessor 需要编译期运行) 4. 确认没有在 `Awake` 之前就触发事件 diff --git a/Runtime/EventBase.cs b/Runtime/EventBase.cs index 189b317..a05528e 100644 --- a/Runtime/EventBase.cs +++ b/Runtime/EventBase.cs @@ -1,5 +1,6 @@ -#nullable enable +#nullable enable using System; +using System.Collections.Concurrent; using System.Reflection; using System.Runtime.Serialization; @@ -7,7 +8,16 @@ namespace ShrinkEventBus { public abstract class EventBase : IDisposable { - private readonly ListenerList _listenerList = new(); + private sealed class EventMetadata + { + public bool IsCancelable; + public bool HasResult; + } + + private static readonly ConcurrentDictionary MetadataCache = new(); + + private EventHandlerInfo[]? _dispatchSnapshot; + private Guid? _eventId; private bool _isCanceled; private EventResult _result = EventResult.DEFAULT; @@ -18,7 +28,7 @@ namespace ShrinkEventBus public DateTime EventTime { get; private set; } = DateTime.UtcNow; [IgnoreDataMember] - public Guid EventId { get; private set; } = Guid.NewGuid(); + public Guid EventId => _eventId ??= Guid.NewGuid(); [IgnoreDataMember] public bool IsCancelable { get; } @@ -34,13 +44,14 @@ namespace ShrinkEventBus protected EventBase() { - IsCancelable = GetType().GetCustomAttribute() != null; - HasResult = GetType().GetCustomAttribute() != null; + var metadata = GetOrCreateMetadata(GetType()); + IsCancelable = metadata.IsCancelable; + HasResult = metadata.HasResult; Setup(); } protected virtual void Setup() { } - + protected virtual void OnReset() { } internal void ResetInternal() @@ -50,8 +61,8 @@ namespace ShrinkEventBus CurrentHandler = null; Phase = null; EventTime = DateTime.UtcNow; - EventId = Guid.NewGuid(); - _listenerList.Clear(); + _eventId = null; + _dispatchSnapshot = null; OnReset(); } @@ -60,8 +71,13 @@ namespace ShrinkEventBus CurrentHandler = null; Phase = null; EventTime = DateTime.UtcNow; - EventId = Guid.NewGuid(); - _listenerList.Clear(); + _eventId = null; + _dispatchSnapshot = null; + } + + internal void SetListenerSnapshot(EventHandlerInfo[]? handlers) + { + _dispatchSnapshot = handlers is { Length: > 0 } ? handlers : null; } [IgnoreDataMember] @@ -70,7 +86,9 @@ namespace ShrinkEventBus get => _isCanceled; set { - if (!IsCancelable) throw new UnsupportedOperationException(); + if (!IsCancelable) + throw new UnsupportedOperationException( + $"Event {GetType().Name} is not cancelable. Mark it with [Cancelable] to allow cancellation."); _isCanceled = value; } } @@ -81,7 +99,9 @@ namespace ShrinkEventBus get => _result; set { - if (!HasResult) throw new InvalidOperationException(); + if (!HasResult) + throw new InvalidOperationException( + $"Event {GetType().Name} does not support results. Mark it with [HasResult] to allow setting a result."); _result = value; } } @@ -90,18 +110,37 @@ namespace ShrinkEventBus { if (Phase == value) return; if (Phase != null && Phase.Value.CompareTo(value) > 0) - throw new ArgumentException(); + throw new ArgumentException( + $"Event phase cannot move backwards from {Phase.Value} to {value}.", nameof(value)); Phase = value; } public void SetCanceled(bool canceled) => IsCanceled = canceled; public void SetResult(EventResult result) => Result = result; - public ListenerList GetListenerList() => _listenerList; - public EventHandlerInfo[] GetSubscribers() => _listenerList.GetHandlers(); + + public EventHandlerInfo[] GetSubscribers() + { + var snapshot = _dispatchSnapshot; + if (snapshot == null || snapshot.Length == 0) + return Array.Empty(); + + var copy = new EventHandlerInfo[snapshot.Length]; + Array.Copy(snapshot, copy, snapshot.Length); + return copy; + } public void Dispose() { ReleaseAction?.Invoke(this); } + + private static EventMetadata GetOrCreateMetadata(Type eventType) + { + return MetadataCache.GetOrAdd(eventType, static type => new EventMetadata + { + IsCancelable = type.GetCustomAttribute() != null, + HasResult = type.GetCustomAttribute() != null + }); + } } } diff --git a/Runtime/EventBus.cs b/Runtime/EventBus.cs index d04e4be..a32a172 100644 --- a/Runtime/EventBus.cs +++ b/Runtime/EventBus.cs @@ -1,398 +1,129 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Reflection; -using System.Runtime.CompilerServices; using Cysharp.Threading.Tasks; -using UnityEngine; namespace ShrinkEventBus { public static class EventBus { - public static event Action OnEventTriggered; + private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus(); + + public static IShrinkEventBus Default => DefaultBus; + + public static event Action OnEventTriggered + { + add => DefaultBus.OnEventTriggered += value; + remove => DefaultBus.OnEventTriggered -= value; + } #if UNITY_EDITOR - public static bool EnableDebugRecord { get; set; } - public static event Action OnEventTriggeredForEditor; + public static bool EnableDebugRecord + { + get => DefaultBus.EnableDebugRecord; + set => DefaultBus.EnableDebugRecord = value; + } + + public static event Action OnEventTriggeredForEditor + { + add => DefaultBus.OnEventTriggeredForEditor += value; + remove => DefaultBus.OnEventTriggeredForEditor -= value; + } #endif - private static readonly Dictionary EventHandlers = new(); - private static readonly HashSet RegisteredInstances = new(); - private static readonly object InstanceLock = new(); + public static ShrinkEventBusBuilder Builder() => new(); - static EventBus() + public static IShrinkEventBus CreateBus(Action? configure = null) { - EventBusRegHelper.RegStaticEventHandler(); + var builder = new ShrinkEventBusBuilder(); + configure?.Invoke(builder); + return builder.Build(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void EnsureAutoManagerInitialized() => EventAutoRegHelper.EnsureInitialized(); + public static void Start() => DefaultBus.Start(); + public static void AutoRegister(object target) => DefaultBus.AutoRegister(target); + public static void Register(object target) => DefaultBus.Register(target); + public static void Unregister(object target) => DefaultBus.Unregister(target); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegisterEvent(Func handler, int priority = 0) - where TEvent : EventBase - { - var eventPriority = PriorityHelper.ConvertToEventPriority(priority); - RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false, - $"Manual Async Handler (Priority: {priority})"); - } + public static void RegisterEvent(Func handler, int priority) + where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegisterEvent(Action handler, EventPriority priority = EventPriority.NORMAL, - bool receiveCanceled = false) where TEvent : EventBase - { - RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled, - $"Manual Sync Handler (Priority: {priority})", handler.Method); - } + public static void RegisterEvent(Action handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) + where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegisterEvent(Action handler, int priority = 0) where TEvent : EventBase - { - var eventPriority = PriorityHelper.ConvertToEventPriority(priority); - RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false, - $"Manual Sync Handler (Priority: {priority})", handler.Method); - } + public static void RegisterEvent(Action handler, int priority) + where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void RegisterEvent(Func handler, - EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) + where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled); + + public static IShrinkEventSubscription SubscribeEvent(Action handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) + where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled); + + public static IShrinkEventSubscription SubscribeEvent(Action handler, int priority) + where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority); + + public static IShrinkEventSubscription SubscribeEvent(Func handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) + where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled); + + public static IShrinkEventSubscription SubscribeEvent(Func handler, int priority) + where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority); + + public static void UnregisterEvent(Func handler) where TEvent : EventBase => + DefaultBus.UnregisterEvent(handler); + + public static void UnregisterEvent(Action handler) where TEvent : EventBase => + DefaultBus.UnregisterEvent(handler); + + public static void ClearAllSubscribersForEvent() where TEvent : EventBase => + DefaultBus.ClearAllSubscribersForEvent(); + + public static void UnregisterAllEventsForObject(object targetObject) => + DefaultBus.UnregisterAllEventsForObject(targetObject); + + public static void UnregisterInstance(object targetObject) => DefaultBus.Unregister(targetObject); + public static void UnregisterAllEvents() => DefaultBus.UnregisterAllEvents(); + + public static UniTask TriggerEventAsync(TEvent eventArgs) where TEvent : EventBase => + DefaultBus.TriggerEventAsync(eventArgs); + + public static UniTask TriggerEventAsync(EventPriority phase, TEvent eventArgs) + where TEvent : EventBase => DefaultBus.TriggerEventAsync(phase, eventArgs); + + public static bool TriggerEvent(TEvent eventArgs) where TEvent : EventBase => + DefaultBus.TriggerEvent(eventArgs); + + public static bool TriggerEvent(EventPriority phase, TEvent eventArgs) where TEvent : EventBase => + DefaultBus.TriggerEvent(phase, eventArgs); + + public static EventHandlerInfo[] GetEventSubscribers() where TEvent : EventBase => + DefaultBus.GetEventSubscribers(); + + public static ListenerList GetListenerList() where TEvent : EventBase => + DefaultBus.GetListenerList(); + + public static IReadOnlyDictionary GetAllSubscribersSnapshot() => + DefaultBus.GetAllSubscribersSnapshot(); + + public static IReadOnlyList GetActiveSubscriptionsSnapshot() => + DefaultBus.GetActiveSubscriptionsSnapshot(); + + public static bool IsInstanceRegistered(object target) => DefaultBus.IsInstanceRegistered(target); + public static int GetRegisteredInstanceCount() => DefaultBus.GetRegisteredInstanceCount(); + public static int GetRegisteredEventTypeCount() => DefaultBus.GetRegisteredEventTypeCount(); + + private static IShrinkEventBus CreateDefaultBus() { - RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled, - $"Manual Async Handler (Priority: {priority})"); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void RegisterEventInternal(Type eventType, Delegate handler, EventPriority priority, - int numericPriority, bool receiveCanceled, string debugInfo = "", MethodInfo originalMethod = null) - { - lock (EventHandlers) - { - if (!EventHandlers.TryGetValue(eventType, out var collection)) - { - collection = new ListenerList(); - EventHandlers[eventType] = collection; - var cacheType = typeof(EventCache<>).MakeGenericType(eventType); - cacheType.GetField("List", BindingFlags.Static | BindingFlags.Public)?.SetValue(null, collection); - } - - collection.Add(handler, priority, numericPriority, receiveCanceled, debugInfo, originalMethod); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterEvent(Func handler) where TEvent : EventBase - { - var eventType = typeof(TEvent); - lock (EventHandlers) - { - if (!EventHandlers.TryGetValue(eventType, out var collection)) return; - collection.Remove(handler); - if (collection.Count == 0) - { - EventHandlers.Remove(eventType); - EventCache.List = null; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterEvent(Action handler) where TEvent : EventBase - { - var eventType = typeof(TEvent); - lock (EventHandlers) - { - if (!EventHandlers.TryGetValue(eventType, out var collection)) return; - collection.Remove(handler); - if (collection.Count == 0) - { - EventHandlers.Remove(eventType); - EventCache.List = null; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ClearAllSubscribersForEvent() where TEvent : EventBase - { - var eventType = typeof(TEvent); - lock (EventHandlers) - { - if (EventHandlers.Remove(eventType)) - EventCache.List = null; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterAllEventsForObject(object targetObject) - { - if (targetObject is null) return; - lock (EventHandlers) - { - var eventTypesToRemove = new List(); - foreach (var kvp in EventHandlers) - { - kvp.Value.RemoveTarget(targetObject); - if (kvp.Value.Count == 0) eventTypesToRemove.Add(kvp.Key); - } - - foreach (var eventType in eventTypesToRemove) - { - EventHandlers.Remove(eventType); - var cacheType = typeof(EventCache<>).MakeGenericType(eventType); - cacheType.GetField("List", BindingFlags.Static | BindingFlags.Public)?.SetValue(null, null); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterInstance(object targetObject) - { - if (targetObject is null) return; - lock (InstanceLock) - { - RegisteredInstances.Remove(targetObject); - } - - UnregisterAllEventsForObject(targetObject); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterAllEvents() - { - lock (EventHandlers) - { - foreach (var type in EventHandlers.Keys) - { - var cacheType = typeof(EventCache<>).MakeGenericType(type); - cacheType.GetField("List", BindingFlags.Static | BindingFlags.Public).SetValue(null, null); - } - - EventHandlers.Clear(); - } - - lock (InstanceLock) - { - RegisteredInstances.Clear(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryPrepareHandler(TEvent eventArgs, EventHandlerInfo handlerInfo) - where TEvent : EventBase - { - eventArgs.CurrentHandler = handlerInfo; - eventArgs.SetPhase(handlerInfo.Priority); - if (eventArgs.IsCancelable && eventArgs.IsCanceled && !handlerInfo.ReceiveCanceled) - return false; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static async UniTask TriggerEventAsync(TEvent eventArgs) where TEvent : EventBase - { - EnsureAutoManagerInitialized(); - eventArgs.PrepareForDispatch(); - var collection = EventCache.List; - var handlers = collection?.GetHandlers() ?? Array.Empty(); - var wasHandled = false; - - for (var i = 0; i < handlers.Length; i++) - { - var handlerInfo = handlers[i]; - if (!TryPrepareHandler(eventArgs, handlerInfo)) continue; - - try - { - if (handlerInfo.Handler is Action syncHandler) - { - syncHandler(eventArgs); - wasHandled = true; - } - else if (handlerInfo.Handler is Func asyncHandler) - { - await asyncHandler(eventArgs); - wasHandled = true; - } - } - catch (Exception ex) - { - Debug.LogException(ex); - Debug.LogError($"[EventBus] Async Handler Exception: {ex.Message}"); - } - } - - eventArgs.CurrentHandler = null; - CompleteEventDispatch(eventArgs, typeof(TEvent), handlers); - return wasHandled; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TriggerEvent(TEvent eventArgs) where TEvent : EventBase - { - EnsureAutoManagerInitialized(); - eventArgs.PrepareForDispatch(); - var collection = EventCache.List; - var handlers = collection?.GetHandlers() ?? Array.Empty(); - var wasHandled = false; - - for (var i = 0; i < handlers.Length; i++) - { - var handlerInfo = handlers[i]; - if (!TryPrepareHandler(eventArgs, handlerInfo)) continue; - - try - { - if (handlerInfo.Handler is Action syncHandler) - { - syncHandler(eventArgs); - wasHandled = true; - } - else if (handlerInfo.Handler is Func asyncHandler) - { - var detachedEvent = EventCloneUtility.CloneForDetachedDispatch(eventArgs); - FireAndForgetSafe(() => asyncHandler(detachedEvent), - $"{handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName}"); - wasHandled = true; - } - } - catch (Exception ex) - { - Debug.LogException(ex); - Debug.LogError($"[EventBus] Sync Handler Exception: {ex.Message}"); - } - } - - eventArgs.CurrentHandler = null; - CompleteEventDispatch(eventArgs, typeof(TEvent), handlers); - return wasHandled; - } - - private static void FireAndForgetSafe(Func action, string handlerName) - { - action().Forget(e => - { - Debug.LogException(e); - Debug.LogError($"[EventBus] Async Handler {handlerName} threw exception: {e.Message}"); - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CompleteEventDispatch(EventBase eventArgs, Type eventType, EventHandlerInfo[] handlers) - { - OnEventTriggered?.Invoke(eventArgs, eventType); - -#if UNITY_EDITOR - if (!EnableDebugRecord) return; - PrepareEditorTrace(handlers, eventArgs); - OnEventTriggeredForEditor?.Invoke(eventArgs, eventType.Name, GetSenderInfo()); -#endif - } - -#if UNITY_EDITOR - private static void PrepareEditorTrace(EventHandlerInfo[] handlers, EventBase eventArgs) - { - if (!EnableDebugRecord) return; - var listenerList = eventArgs.GetListenerList(); - listenerList.Clear(); - for (var i = 0; i < handlers.Length; i++) - listenerList.Add(handlers[i]); - } - - private static string GetSenderInfo() - { - var senderInfo = "Unknown"; - try - { - var trace = new System.Diagnostics.StackTrace(0, false); - for (var i = 0; i < trace.FrameCount; i++) - { - var method = trace.GetFrame(i)?.GetMethod(); - var declaringType = method?.DeclaringType; - if (declaringType == null) continue; - if (declaringType == typeof(EventBus) || declaringType.DeclaringType == typeof(EventBus)) continue; - var ns = declaringType.Namespace ?? ""; - if (ns.StartsWith("System") || ns.StartsWith("Cysharp") || ns.StartsWith("UnityEngine")) continue; - var className = declaringType.Name; - var methodName = method.Name; - if (declaringType.DeclaringType != null && className.StartsWith("<") && className.Contains(">")) - { - className = declaringType.DeclaringType.Name; - var startIndex = declaringType.Name.IndexOf('<') + 1; - var endIndex = declaringType.Name.IndexOf('>'); - if (startIndex > 0 && endIndex > startIndex) - methodName = declaringType.Name.Substring(startIndex, endIndex - startIndex); - } - - return $"{className}.{methodName}()"; - } - } - catch - { - } - - return senderInfo; - } -#endif - - public static EventHandlerInfo[] GetEventSubscribers() where TEvent : EventBase - { - var eventType = typeof(TEvent); - lock (EventHandlers) - { - return EventHandlers.TryGetValue(eventType, out var collection) - ? collection.GetHandlers() - : Array.Empty(); - } - } - - public static ListenerList GetListenerList() where TEvent : EventBase - { - var eventType = typeof(TEvent); - lock (EventHandlers) - { - EventHandlers.TryGetValue(eventType, out var collection); - return collection; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void AutoRegister(object target) - { - if (target == null) return; - if (IsInstanceRegistered(target)) return; - EventBusRegHelper.RegisterEventHandlers(target); - lock (InstanceLock) - { - if (!RegisteredInstances.Add(target)) return; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsInstanceRegistered(object target) - { - if (target == null) return false; - lock (InstanceLock) - { - return RegisteredInstances.Contains(target); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetRegisteredInstanceCount() - { - lock (InstanceLock) - { - return RegisteredInstances.Count; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetRegisteredEventTypeCount() - { - lock (EventHandlers) - { - return EventHandlers.Count; - } + var bus = (ShrinkEventBusInstance)new ShrinkEventBusBuilder() + .SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndContinue) + .AllowPerPhaseDispatch() + .Build(); + EventBusRegHelper.RegStaticEventHandler(bus); + return bus; } } } diff --git a/Runtime/EventBusBenchmark.cs b/Runtime/EventBusBenchmark.cs index 63ce6db..bc5c98e 100644 --- a/Runtime/EventBusBenchmark.cs +++ b/Runtime/EventBusBenchmark.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Diagnostics; +using System.Reflection; using System.Text; using Cysharp.Threading.Tasks; using UnityEngine; @@ -8,39 +9,98 @@ using Debug = UnityEngine.Debug; namespace ShrinkEventBus.Runtime { /// - /// ShrinkEventBus 性能测试 + /// ShrinkEventBus 性能测试。 /// 挂在任意 GameObject 上,运行后在 Console 查看结果。 - /// 在进行测试时不要打开事件查看器大的追踪实时事件功能 + /// 在进行测试时不要打开事件查看器的大量实时追踪。 /// - public class EventBusBenchmark : MonoBehaviour + public sealed class EventBusBenchmark : MonoBehaviour { - [Header("同步触发测试")] public int iterations = 1000000; + [Header("同步触发测试")] public int iterations = 1_000_000; [Header("大批量订阅者测试")] public int subscriberCount = 30; - public int massIterations = 1000; + public int massIterations = 10_000; - [Header("异步触发测试")] public int asyncIterations = 1000; + [Header("异步触发测试")] public int asyncIterations = 1_000; - [Header("注册 / 注销测试")] public int registerIterations = 5000; - - [Header("取消事件测试")] public int cancelIterations = 100000; + [Header("注册 / 注销测试")] public int registerIterations = 5_000; + public int scannedRegisterIterations = 1_000; + [Header("取消事件测试")] public int cancelIterations = 100_000; public class BenchmarkEvent : EventBase { public int Value { get; set; } } + public sealed class DerivedBenchmarkEvent : BenchmarkEvent + { + public bool IsDerived { get; set; } + } + [Cancelable] public class CancelableBenchmarkEvent : EventBase { public int Value { get; set; } } + private sealed class BenchmarkInstanceSubscriber + { + public int Recorded; + + [EventSubscribe(EventPriority.NORMAL)] + private void OnBenchmark(BenchmarkEvent evt) + { + Recorded = evt.Value; + } + } + + private static class BenchmarkStaticSubscriber + { + public static int Recorded; + + [EventSubscribe(EventPriority.NORMAL)] + private static void OnBenchmark(BenchmarkEvent evt) + { + Recorded = evt.Value; + } + } + + private static class BenchmarkMethodSubscriber + { + public static int Recorded; + + [EventSubscribe(EventPriority.NORMAL)] + private static void OnBenchmark(BenchmarkEvent evt) + { + Recorded = evt.Value; + } + } + + private sealed class PhaseSubscriber + { + public int Sum; + + [EventSubscribe(EventPriority.HIGH)] + private void OnHigh(BenchmarkEvent evt) + { + Sum += evt.Value + 1; + } + + [EventSubscribe(EventPriority.NORMAL)] + private void OnNormal(BenchmarkEvent evt) + { + Sum += evt.Value + 10; + } + + [EventSubscribe(EventPriority.LOW)] + private void OnLow(BenchmarkEvent evt) + { + Sum += evt.Value + 100; + } + } private readonly StringBuilder _report = new(); - private void Start() { RunAllBenchmarks().Forget(); @@ -52,8 +112,9 @@ namespace ShrinkEventBus.Runtime _report.AppendLine("╔══════════════════════════════════════════════════╗"); _report.AppendLine("║ ShrinkEventBus Benchmark Report ║"); _report.AppendLine( - $"║ iter={iterations,8} mass={massIterations,5} reg={registerIterations,5} cancel={cancelIterations,6} ║"); + $"║ iter={iterations,8} mass={massIterations,6} reg={registerIterations,5} scan={scannedRegisterIterations,4} ║"); _report.AppendLine("╚══════════════════════════════════════════════════╝"); + _report.AppendLine(" 基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启)"); Log("开始:无订阅者触发"); Bench_TriggerEvent_NoSubscriber(); @@ -63,16 +124,36 @@ namespace ShrinkEventBus.Runtime Bench_TriggerEvent_SingleSubscriber(); await UniTask.Yield(); + Log("开始:父事件监听子事件"); + Bench_TriggerEvent_InheritedSubscriber(); + await UniTask.Yield(); + Log($"开始:{subscriberCount} 订阅者同步触发"); Bench_TriggerEvent_MassSubscribers(); await UniTask.Yield(); + Log("开始:按 phase 分发"); + Bench_TriggerEvent_PerPhase(); + await UniTask.Yield(); + Log("开始:异步触发"); await Bench_TriggerEventAsync(); await UniTask.Yield(); - Log("开始:注册/注销"); - Bench_RegisterUnregister(); + Log("开始:手工 delegate 注册/注销"); + Bench_RegisterUnregister_ManualDelegate(); + await UniTask.Yield(); + + Log("开始:实例扫描注册/注销"); + Bench_RegisterUnregister_InstanceScan(); + await UniTask.Yield(); + + Log("开始:静态类型扫描注册/注销"); + Bench_RegisterUnregister_StaticScan(); + await UniTask.Yield(); + + Log("开始:MethodInfo 注册/注销"); + Bench_RegisterUnregister_MethodScan(); await UniTask.Yield(); Log("开始:EventPool vs new"); @@ -87,95 +168,141 @@ namespace ShrinkEventBus.Runtime Debug.Log(_report.ToString()); } - private void Bench_TriggerEvent_NoSubscriber() { - EventBus.ClearAllSubscribersForEvent(); + var bus = CreateBenchmarkBus(); var evt = new BenchmarkEvent { Value = 1 }; - for (var i = 0; i < 500; i++) EventBus.TriggerEvent(evt); + Warmup(() => bus.TriggerEvent(evt), 500); var sw = Stopwatch.StartNew(); - for (var i = 0; i < iterations; i++) EventBus.TriggerEvent(evt); + for (var i = 0; i < iterations; i++) + bus.TriggerEvent(evt); sw.Stop(); Record("TriggerEvent × 无订阅者(基线)", sw.Elapsed.TotalMilliseconds, iterations); Log("完成:无订阅者触发"); } - private void Bench_TriggerEvent_SingleSubscriber() { - EventBus.ClearAllSubscribersForEvent(); - + var bus = CreateBenchmarkBus(); var dummy = 0; - void Handler(BenchmarkEvent e) + void Handler(BenchmarkEvent evt) { - dummy = e.Value; + dummy = evt.Value; } - EventBus.RegisterEvent(Handler, EventPriority.NORMAL); + bus.RegisterEvent(Handler, EventPriority.NORMAL); var evt = new BenchmarkEvent { Value = 1 }; - for (var i = 0; i < 500; i++) EventBus.TriggerEvent(evt); + Warmup(() => bus.TriggerEvent(evt), 500); var sw = Stopwatch.StartNew(); - for (var i = 0; i < iterations; i++) EventBus.TriggerEvent(evt); + for (var i = 0; i < iterations; i++) + bus.TriggerEvent(evt); sw.Stop(); Record("TriggerEvent × 单订阅者同步", sw.Elapsed.TotalMilliseconds, iterations); Log("完成:单订阅者同步触发"); - EventBus.UnregisterEvent(Handler); + bus.UnregisterEvent(Handler); GC.KeepAlive(dummy); } + private void Bench_TriggerEvent_InheritedSubscriber() + { + var bus = CreateBenchmarkBus(); + var dummy = 0; + + void Handler(BenchmarkEvent evt) + { + dummy = evt.Value; + } + + bus.RegisterEvent(Handler, EventPriority.NORMAL); + + var evt = new DerivedBenchmarkEvent { Value = 1, IsDerived = true }; + Warmup(() => bus.TriggerEvent(evt), 500); + + var sw = Stopwatch.StartNew(); + for (var i = 0; i < iterations; i++) + bus.TriggerEvent(evt); + sw.Stop(); + + Record("TriggerEvent × 父事件监听子事件", sw.Elapsed.TotalMilliseconds, iterations); + Log("完成:父事件监听子事件"); + + bus.UnregisterEvent(Handler); + GC.KeepAlive(dummy); + } private void Bench_TriggerEvent_MassSubscribers() { - EventBus.ClearAllSubscribersForEvent(); - + var bus = CreateBenchmarkBus(); var dummy = 0; var handlers = new Action[subscriberCount]; for (var i = 0; i < subscriberCount; i++) { var captured = i; - handlers[i] = e => { dummy = captured + e.Value; }; - EventBus.RegisterEvent(handlers[i], EventPriority.NORMAL); + handlers[i] = evt => { dummy = captured + evt.Value; }; + bus.RegisterEvent(handlers[i], EventPriority.NORMAL); } var evt = new BenchmarkEvent { Value = 1 }; - for (var i = 0; i < 100; i++) EventBus.TriggerEvent(evt); + Warmup(() => bus.TriggerEvent(evt), 100); var sw = Stopwatch.StartNew(); - for (var i = 0; i < massIterations; i++) EventBus.TriggerEvent(evt); + for (var i = 0; i < massIterations; i++) + bus.TriggerEvent(evt); sw.Stop(); Record($"TriggerEvent × {subscriberCount} 订阅者同步", sw.Elapsed.TotalMilliseconds, massIterations); Log($"完成:{subscriberCount} 订阅者同步触发"); - foreach (var h in handlers) EventBus.UnregisterEvent(h); + foreach (var handler in handlers) + bus.UnregisterEvent(handler); + GC.KeepAlive(dummy); } + private void Bench_TriggerEvent_PerPhase() + { + var bus = CreateBenchmarkBus(); + var subscriber = new PhaseSubscriber(); + bus.Register(subscriber); + + var evt = new BenchmarkEvent { Value = 1 }; + Warmup(() => bus.TriggerEvent(EventPriority.HIGH, evt), 500); + + var sw = Stopwatch.StartNew(); + for (var i = 0; i < iterations; i++) + bus.TriggerEvent(EventPriority.HIGH, evt); + sw.Stop(); + + Record("TriggerEvent(Phase=HIGH) × 3 phase 监听", sw.Elapsed.TotalMilliseconds, iterations); + Log("完成:按 phase 分发"); + + bus.Unregister(subscriber); + GC.KeepAlive(subscriber.Sum); + } private async UniTask Bench_TriggerEventAsync() { - EventBus.ClearAllSubscribersForEvent(); - + var bus = CreateBenchmarkBus(); var dummy = 0; - async UniTask Handler(BenchmarkEvent e) + async UniTask Handler(BenchmarkEvent evt) { - dummy = e.Value; + dummy = evt.Value; await UniTask.CompletedTask; } - EventBus.RegisterEvent(Handler, EventPriority.NORMAL); + bus.RegisterEvent(Handler, EventPriority.NORMAL); var evt = new BenchmarkEvent { Value = 1 }; - for (var i = 0; i < 10; i++) await EventBus.TriggerEventAsync(evt); + await WarmupAsync(() => bus.TriggerEventAsync(evt), 10); const int perFrame = 10; var totalMs = 0.0; @@ -186,7 +313,8 @@ namespace ShrinkEventBus.Runtime await UniTask.Yield(); var count = Math.Min(perFrame, asyncIterations - ran); var sw = Stopwatch.StartNew(); - for (var i = 0; i < count; i++) await EventBus.TriggerEventAsync(evt); + for (var i = 0; i < count; i++) + await bus.TriggerEventAsync(evt); sw.Stop(); totalMs += sw.Elapsed.TotalMilliseconds; ran += count; @@ -195,48 +323,102 @@ namespace ShrinkEventBus.Runtime Record("TriggerEventAsync × 单订阅者异步", totalMs, asyncIterations); Log("完成:异步触发"); - EventBus.UnregisterEvent(Handler); + bus.UnregisterEvent(Handler); GC.KeepAlive(dummy); } - - private void Bench_RegisterUnregister() + private void Bench_RegisterUnregister_ManualDelegate() { + var bus = CreateBenchmarkBus(); var dummy = 0; var handlers = new Action[registerIterations]; for (var i = 0; i < registerIterations; i++) { var captured = i; - handlers[i] = e => { dummy = captured; }; + handlers[i] = evt => { dummy = captured; }; } var swReg = Stopwatch.StartNew(); for (var i = 0; i < registerIterations; i++) - EventBus.RegisterEvent(handlers[i], EventPriority.NORMAL); + bus.RegisterEvent(handlers[i], EventPriority.NORMAL); swReg.Stop(); - Record("RegisterEvent", swReg.Elapsed.TotalMilliseconds, registerIterations); - Log("完成:RegisterEvent"); + Record("RegisterEvent(delegate)", swReg.Elapsed.TotalMilliseconds, registerIterations); var swUnreg = Stopwatch.StartNew(); for (var i = 0; i < registerIterations; i++) - EventBus.UnregisterEvent(handlers[i]); + bus.UnregisterEvent(handlers[i]); swUnreg.Stop(); - Record("UnregisterEvent", swUnreg.Elapsed.TotalMilliseconds, registerIterations); - Log("完成:UnregisterEvent"); + Record("UnregisterEvent(delegate)", swUnreg.Elapsed.TotalMilliseconds, registerIterations); + Log("完成:手工 delegate 注册/注销"); GC.KeepAlive(dummy); } + private void Bench_RegisterUnregister_InstanceScan() + { + var bus = CreateBenchmarkBus(); + var subscribers = new BenchmarkInstanceSubscriber[scannedRegisterIterations]; + for (var i = 0; i < subscribers.Length; i++) + subscribers[i] = new BenchmarkInstanceSubscriber(); + + var sw = Stopwatch.StartNew(); + for (var i = 0; i < subscribers.Length; i++) + { + var subscriber = subscribers[i]; + bus.Register(subscriber); + bus.Unregister(subscriber); + } + + sw.Stop(); + Record("Register/Unregister(instance scan)", sw.Elapsed.TotalMilliseconds, subscribers.Length); + Log("完成:实例扫描注册/注销"); + } + + private void Bench_RegisterUnregister_StaticScan() + { + var bus = CreateBenchmarkBus(); + + var sw = Stopwatch.StartNew(); + for (var i = 0; i < scannedRegisterIterations; i++) + { + bus.Register(typeof(BenchmarkStaticSubscriber)); + bus.Unregister(typeof(BenchmarkStaticSubscriber)); + } + + sw.Stop(); + Record("Register/Unregister(static type scan)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations); + Log("完成:静态类型扫描注册/注销"); + } + + private void Bench_RegisterUnregister_MethodScan() + { + var bus = CreateBenchmarkBus(); + var method = typeof(BenchmarkMethodSubscriber).GetMethod("OnBenchmark", + BindingFlags.Static | BindingFlags.NonPublic); + if (method == null) + throw new MissingMethodException(typeof(BenchmarkMethodSubscriber).FullName, "OnBenchmark"); + + var sw = Stopwatch.StartNew(); + for (var i = 0; i < scannedRegisterIterations; i++) + { + bus.Register(method); + bus.Unregister(method); + } + + sw.Stop(); + Record("Register/Unregister(MethodInfo)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations); + Log("完成:MethodInfo 注册/注销"); + } private void Bench_EventPool_vs_New() { - EventBus.ClearAllSubscribersForEvent(); + var bus = CreateBenchmarkBus(); - void Handler(BenchmarkEvent e) + void Handler(BenchmarkEvent evt) { } - EventBus.RegisterEvent(Handler, EventPriority.NORMAL); + bus.RegisterEvent(Handler, EventPriority.NORMAL); for (var i = 0; i < 64; i++) EventPool.Release(EventPool.Get()); @@ -246,75 +428,92 @@ namespace ShrinkEventBus.Runtime var swNew = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) { - var e = new BenchmarkEvent { Value = i }; - EventBus.TriggerEvent(e); + var evt = new BenchmarkEvent { Value = i }; + bus.TriggerEvent(evt); } swNew.Stop(); var gcNew = GC.CollectionCount(0) - gcBefore; Record($"TriggerEvent × new() [GC Gen0={gcNew,3}]", swNew.Elapsed.TotalMilliseconds, iterations); - Log("完成:new() 分配"); GC.Collect(); gcBefore = GC.CollectionCount(0); var swPool = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) { - using var e = EventPool.Get(); - e.Value = i; - EventBus.TriggerEvent(e); + using var evt = EventPool.Get(); + evt.Value = i; + bus.TriggerEvent(evt); } swPool.Stop(); var gcPool = GC.CollectionCount(0) - gcBefore; Record($"TriggerEvent × Pool.Get() [GC Gen0={gcPool,3}]", swPool.Elapsed.TotalMilliseconds, iterations); - Log("完成:Pool.Get()"); + Log("完成:EventPool vs new"); - EventBus.UnregisterEvent(Handler); + bus.UnregisterEvent(Handler); } - private void Bench_CanceledEvent_Skip() { - EventBus.ClearAllSubscribersForEvent(); - + var bus = CreateBenchmarkBus(); var dummy = 0; var handlers = new Action[10]; - for (var i = 0; i < 10; i++) + for (var i = 0; i < handlers.Length; i++) { - handlers[i] = e => { dummy = e.Value; }; - EventBus.RegisterEvent(handlers[i], EventPriority.NORMAL); + handlers[i] = evt => { dummy = evt.Value; }; + bus.RegisterEvent(handlers[i], EventPriority.NORMAL); } var evt = new CancelableBenchmarkEvent { Value = 1 }; evt.SetCanceled(true); - for (var i = 0; i < 500; i++) EventBus.TriggerEvent(evt); + Warmup(() => bus.TriggerEvent(evt), 500); var sw = Stopwatch.StartNew(); - for (var i = 0; i < cancelIterations; i++) EventBus.TriggerEvent(evt); + for (var i = 0; i < cancelIterations; i++) + bus.TriggerEvent(evt); sw.Stop(); Record("TriggerEvent × 已取消事件跳过(10 订阅者)", sw.Elapsed.TotalMilliseconds, cancelIterations); Log("完成:已取消事件跳过"); - foreach (var h in handlers) - EventBus.UnregisterEvent(h); + foreach (var handler in handlers) + bus.UnregisterEvent(handler); + GC.KeepAlive(dummy); } + private static IShrinkEventBus CreateBenchmarkBus() + { + return EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch()); + } - private static void Log(string msg) => + private static void Warmup(Action action, int count) + { + for (var i = 0; i < count; i++) + action(); + } + + private static async UniTask WarmupAsync(Func> action, int count) + { + for (var i = 0; i < count; i++) + await action(); + } + + private static void Log(string msg) + { Debug.Log($"[Benchmark] {msg}"); + } private void Record(string label, double totalMs, int count) { var perOp = totalMs / count * 1000.0; - var throughput = count / (totalMs / 1000.0); + var throughput = totalMs <= 0.0001 ? 0 : count / (totalMs / 1000.0); _report.AppendLine($"\n ▶ {label}"); _report.AppendLine($" 总耗时 : {totalMs,10:F3} ms"); _report.AppendLine($" 单次 : {perOp,10:F4} μs/op"); _report.AppendLine($" 吞吐量 : {throughput,10:F0} ops/s"); } } -} \ No newline at end of file +} diff --git a/Runtime/EventBusGeneratedRegistry.cs b/Runtime/EventBusGeneratedRegistry.cs new file mode 100644 index 0000000..9883a9a --- /dev/null +++ b/Runtime/EventBusGeneratedRegistry.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ShrinkEventBus +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class EventBusStaticRegistryAttribute : Attribute + { + public EventBusStaticRegistryAttribute(params Type[] subscriberTypes) + { + SubscriberTypes = subscriberTypes ?? Array.Empty(); + } + + public Type[] SubscriberTypes { get; } + } + + internal static class EventBusGeneratedRegistry + { + public static IReadOnlyList GetStaticSubscriberTypes() + { + var types = new List(); + var seen = new HashSet(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + object[] attributes; + try + { + attributes = assembly.GetCustomAttributes(typeof(EventBusStaticRegistryAttribute), false); + } + catch + { + continue; + } + + foreach (var attribute in attributes.OfType()) + { + foreach (var subscriberType in attribute.SubscriberTypes ?? Array.Empty()) + { + if (subscriberType == null || !seen.Add(subscriberType)) + continue; + + types.Add(subscriberType); + } + } + } + + return types; + } + } +} diff --git a/Runtime/EventBusGeneratedRegistry.cs.meta b/Runtime/EventBusGeneratedRegistry.cs.meta new file mode 100644 index 0000000..f6c67f8 --- /dev/null +++ b/Runtime/EventBusGeneratedRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04809187aa0ce3b43a4df12b048a1c98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/EventBusRegHelper.cs b/Runtime/EventBusRegHelper.cs index e231f06..bbff28f 100644 --- a/Runtime/EventBusRegHelper.cs +++ b/Runtime/EventBusRegHelper.cs @@ -1,4 +1,6 @@ +#nullable enable using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using Cysharp.Threading.Tasks; @@ -6,104 +8,185 @@ using UnityEngine; namespace ShrinkEventBus { - public static class EventBusRegHelper + internal static class EventBusRegHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegStaticEventHandler() => RegisterEventHandlersInternal(null); + public static void RegStaticEventHandler(ShrinkEventBusInstance bus) + { + if (bus == null) + throw new ArgumentNullException(nameof(bus)); + + RegisterStaticSubscriberTypes(bus, EventBusGeneratedRegistry.GetStaticSubscriberTypes()); + } + + public static void RegisterStaticSubscriberTypes(ShrinkEventBusInstance bus, IEnumerable subscriberTypes) + { + if (bus == null) + throw new ArgumentNullException(nameof(bus)); + if (subscriberTypes == null) + throw new ArgumentNullException(nameof(subscriberTypes)); + + foreach (var type in subscriberTypes) + { + try + { + if (type == null || type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0) + continue; + + RegisterTarget(bus, type, requireSubscriberAttribute: true, lenientWhenNoMethods: true); + } + catch (Exception ex) + { +#if UNITY_EDITOR + Debug.LogWarning($"[EventBus] Static registration failed for {type?.FullName}: {ex.Message}"); +#endif + } + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegisterEventHandlers(object target) => RegisterEventHandlersInternal(target); - - private static void RegisterEventHandlersInternal(object target) + public static void RegisterTarget(ShrinkEventBusInstance bus, object target, bool requireSubscriberAttribute, + bool lenientWhenNoMethods = false) { - var isStatic = target == null; + if (bus == null) + throw new ArgumentNullException(nameof(bus)); + if (target == null) + throw new ArgumentNullException(nameof(target)); - if (isStatic) + switch (target) { - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var assembly in assemblies) - { - try - { - foreach (var type in assembly.GetTypes()) - { - if (type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0) - continue; - ScanMethodsAndRegister(null, - type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); - } - } - catch - { - // ignored - } - } - } - else - { - var type = target.GetType(); - if (type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0) return; - ScanMethodsAndRegister(target, - type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)); + case MethodInfo method: + RegisterMethod(bus, null, method, requireSubscriberAttribute: false, isStaticRegistration: true); + return; + case Type staticType: + RegisterDeclaredMethods(bus, null, staticType, requireSubscriberAttribute, + isStaticRegistration: true, lenientWhenNoMethods); + return; + default: + RegisterDeclaredMethods(bus, target, target.GetType(), requireSubscriberAttribute, + isStaticRegistration: false, lenientWhenNoMethods); + return; } } - private static void ScanMethodsAndRegister(object target, MethodInfo[] methods) + private static void RegisterDeclaredMethods(ShrinkEventBusInstance bus, object? target, Type type, + bool requireSubscriberAttribute, bool isStaticRegistration, bool lenientWhenNoMethods) { + if (requireSubscriberAttribute && + type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0) + { + throw new InvalidOperationException( + $"Type {type.FullName} must declare [EventBusSubscriber] before it can be auto-registered."); + } + + var flags = (isStaticRegistration ? BindingFlags.Static : BindingFlags.Instance) | + BindingFlags.Public | BindingFlags.NonPublic; + var methods = type.GetMethods(flags); + var foundMethods = 0; foreach (var method in methods) { - if (target != null && method.IsStatic) continue; - var attributes = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false); - if (attributes.Length == 0) continue; - var subscribeAttr = (EventSubscribeAttribute)attributes[0]; - var parameters = method.GetParameters(); - if (parameters.Length != 1) continue; - var parameterType = parameters[0].ParameterType; - if (!typeof(EventBase).IsAssignableFrom(parameterType)) continue; - ProcessMethodRegistration(target, method, subscribeAttr, parameterType); + var hasSubscribeAttribute = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false).Length > 0; + if (!hasSubscribeAttribute) + continue; + + if (method.IsStatic != isStaticRegistration) + { + var expected = isStaticRegistration ? "static" : "instance"; + throw new InvalidOperationException( + $"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode."); + } + + RegisterMethod(bus, target, method, requireSubscriberAttribute: false, isStaticRegistration); + foundMethods++; + } + + if (foundMethods == 0) + { + var message = + $"Type {type.FullName} has no [EventSubscribe] methods for {(isStaticRegistration ? "static" : "instance")} registration."; + if (lenientWhenNoMethods) + { + Debug.LogWarning($"[EventBus] {message} Auto-registration skipped."); + return; + } + + throw new InvalidOperationException(message); } } - private static void ProcessMethodRegistration(object target, MethodInfo method, + private static void RegisterMethod(ShrinkEventBusInstance bus, object? target, MethodInfo method, + bool requireSubscriberAttribute, bool isStaticRegistration) + { + if (method == null) + throw new ArgumentNullException(nameof(method)); + + if (!method.IsDefined(typeof(EventSubscribeAttribute), false)) + throw new InvalidOperationException($"Method {method} is not annotated with [EventSubscribe]."); + + if (method.IsStatic != isStaticRegistration) + { + var expected = isStaticRegistration ? "static" : "instance"; + throw new InvalidOperationException( + $"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode."); + } + + if (requireSubscriberAttribute && method.DeclaringType != null && + method.DeclaringType.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0) + { + throw new InvalidOperationException( + $"Type {method.DeclaringType.FullName} must declare [EventBusSubscriber] before it can be auto-registered."); + } + + var subscribeAttr = (EventSubscribeAttribute)method.GetCustomAttributes(typeof(EventSubscribeAttribute), false)[0]; + var parameters = method.GetParameters(); + if (parameters.Length != 1) + { + throw new InvalidOperationException( + $"Method {method} has [EventSubscribe] but declares {parameters.Length} parameters. Event handlers must declare exactly one EventBase parameter."); + } + + var parameterType = parameters[0].ParameterType; + if (!typeof(EventBase).IsAssignableFrom(parameterType)) + { + throw new InvalidOperationException( + $"Method {method} has [EventSubscribe] but parameter {parameterType.FullName} does not inherit from EventBase."); + } + + ProcessMethodRegistration(bus, target, method, subscribeAttr, parameterType); + } + + private static void ProcessMethodRegistration(ShrinkEventBusInstance bus, object? target, MethodInfo method, EventSubscribeAttribute subscribeAttr, Type parameterType) { - try - { - string scope = target == null ? "Static" : "Instance"; - string typeName = target == null ? method.DeclaringType?.Name : target.GetType().Name; + string scope = target == null ? "Static" : "Instance"; + string typeName = target == null ? method.DeclaringType?.Name ?? "Unknown" : target.GetType().Name; - if (method.ReturnType == typeof(UniTask)) - { - var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(UniTask)); - var handlerDelegate = target == null - ? Delegate.CreateDelegate(funcType, method) - : Delegate.CreateDelegate(funcType, target, method); - EventBus.RegisterEventInternal(parameterType, handlerDelegate, subscribeAttr.Priority, - subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled, - $"{scope} {typeName}.{method.Name} (UniTask)", method); - } - else if (method.ReturnType == typeof(void)) - { - var actionType = typeof(Action<>).MakeGenericType(parameterType); - var actionDelegate = target == null - ? Delegate.CreateDelegate(actionType, method) - : Delegate.CreateDelegate(actionType, target, method); - EventBus.RegisterEventInternal(parameterType, actionDelegate, subscribeAttr.Priority, - subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled, - $"{scope} {typeName}.{method.Name} (Sync)", method); - } - else - { - Debug.LogWarning( - $"[EventBus] Registration failed: {typeName}.{method.Name} return type must be void or UniTask."); - } - } - catch (Exception ex) + if (method.ReturnType == typeof(UniTask)) { -#if UNITY_EDITOR - Debug.LogWarning($"[EventBus] Registration failed for {method.Name}: {ex.Message}"); -#endif + var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(UniTask)); + var handlerDelegate = target == null + ? Delegate.CreateDelegate(funcType, method) + : Delegate.CreateDelegate(funcType, target, method); + bus.RegisterEventInternal(parameterType, handlerDelegate, subscribeAttr.Priority, + subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled, + $"{scope} {typeName}.{method.Name} (UniTask)", method); + return; } + + if (method.ReturnType == typeof(void)) + { + var actionType = typeof(Action<>).MakeGenericType(parameterType); + var actionDelegate = target == null + ? Delegate.CreateDelegate(actionType, method) + : Delegate.CreateDelegate(actionType, target, method); + bus.RegisterEventInternal(parameterType, actionDelegate, subscribeAttr.Priority, + subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled, + $"{scope} {typeName}.{method.Name} (Sync)", method); + return; + } + + throw new InvalidOperationException( + $"Method {method} has [EventSubscribe] but return type {method.ReturnType.FullName} is unsupported. Use void or UniTask."); } } -} \ No newline at end of file +} diff --git a/Runtime/EventCache.cs b/Runtime/EventCache.cs deleted file mode 100644 index e59816f..0000000 --- a/Runtime/EventCache.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ShrinkEventBus -{ - internal static class EventCache where TEvent : EventBase - { - public static ListenerList List; - } -} \ No newline at end of file diff --git a/Runtime/EventCache.cs.meta b/Runtime/EventCache.cs.meta deleted file mode 100644 index 7fad8e5..0000000 --- a/Runtime/EventCache.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 0c41a516998341adbddce082cec09938 -timeCreated: 1772889485 \ No newline at end of file diff --git a/Runtime/EventCloneUtility.cs b/Runtime/EventCloneUtility.cs index 393f92a..96aaac4 100644 --- a/Runtime/EventCloneUtility.cs +++ b/Runtime/EventCloneUtility.cs @@ -18,8 +18,9 @@ namespace ShrinkEventBus throw new InvalidOperationException( $"Cannot clone event type {source.GetType().FullName}. A public parameterless constructor is required."); + // 先固化 EventId,让克隆与原事件共享同一个派发标识 + _ = source.EventId; CopyFields(source, clone); - CopyListenerSnapshot(source, clone); clone.ReleaseAction = null; clone.IsInPool = false; return clone; @@ -32,16 +33,6 @@ namespace ShrinkEventBus fields[i].SetValue(target, fields[i].GetValue(source)); } - private static void CopyListenerSnapshot(EventBase source, EventBase target) - { - var targetListeners = target.GetListenerList(); - targetListeners.Clear(); - - var sourceHandlers = source.GetListenerList().GetHandlers(); - for (var i = 0; i < sourceHandlers.Length; i++) - targetListeners.Add(sourceHandlers[i]); - } - private static FieldInfo[] GetCopyableFields(Type type) { lock (CacheLock) @@ -77,8 +68,7 @@ namespace ShrinkEventBus private static bool ShouldSkipField(FieldInfo field) { - return field.Name is "_listenerList" - or "k__BackingField" + return field.Name is "k__BackingField" or "k__BackingField"; } } diff --git a/Runtime/EventHandlerInfo.cs b/Runtime/EventHandlerInfo.cs index 0628008..30cf122 100644 --- a/Runtime/EventHandlerInfo.cs +++ b/Runtime/EventHandlerInfo.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Cysharp.Threading.Tasks; #pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。 @@ -12,7 +13,10 @@ namespace ShrinkEventBus public class EventHandlerInfo { + public long SubscriptionId { get; } public Delegate Handler { get; } + internal Action? SyncInvoker { get; } + internal Func? AsyncInvoker { get; } public EventPriority Priority { get; } public int NumericPriority { get; } public bool ReceiveCanceled { get; } @@ -24,11 +28,17 @@ namespace ShrinkEventBus public MethodInfo? OriginalMethod { get; } public string OriginalMethodName { get; } public Type? OriginalDeclaringType { get; } + public DateTime RegisteredAtUtc { get; } - public EventHandlerInfo(Delegate handler, EventPriority priority, int numericPriority, - bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null) + private EventHandlerInfo(long subscriptionId, Delegate handler, Action? syncInvoker, + Func? asyncInvoker, + EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "", + MethodInfo? originalMethod = null) { + SubscriptionId = subscriptionId; Handler = handler; + SyncInvoker = syncInvoker; + AsyncInvoker = asyncInvoker; Priority = priority; NumericPriority = numericPriority; ReceiveCanceled = receiveCanceled; @@ -50,6 +60,8 @@ namespace ShrinkEventBus OriginalMethodName = MethodName; OriginalDeclaringType = DeclaringType; } + + RegisteredAtUtc = DateTime.UtcNow; } private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler) @@ -64,5 +76,66 @@ namespace ShrinkEventBus public string DisplayMethodName => OriginalMethodName; public Type DisplayDeclaringType => OriginalDeclaringType ?? DeclaringType; + + public bool MatchesMethod(MethodInfo method) + { + return method != null && (ReferenceEquals(Method, method) || ReferenceEquals(OriginalMethod, method)); + } + + public bool MatchesDeclaringType(Type declaringType) + { + return declaringType != null && + (DeclaringType == declaringType || OriginalDeclaringType == declaringType); + } + + public static EventHandlerInfo Create(long subscriptionId, Delegate handler, Type eventType, + EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "", + MethodInfo? originalMethod = null) + { + if (handler == null) + throw new System.ArgumentNullException(nameof(handler)); + if (eventType == null) + throw new System.ArgumentNullException(nameof(eventType)); + + Action? syncInvoker = null; + Func? asyncInvoker = null; + if (handler.Method.ReturnType == typeof(void)) + syncInvoker = CreateSyncInvoker(eventType, handler); + else if (handler.Method.ReturnType == typeof(UniTask)) + asyncInvoker = CreateAsyncInvoker(eventType, handler); + else + throw new System.ArgumentException( + $"Unsupported event handler return type {handler.Method.ReturnType.FullName} for {handler.Method}."); + + return new EventHandlerInfo(subscriptionId, handler, syncInvoker, asyncInvoker, priority, numericPriority, + receiveCanceled, debugInfo, originalMethod); + } + + private static Action CreateSyncInvoker(Type eventType, Delegate handler) + { + var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateSyncInvokerGeneric), + BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType); + return (Action)factory.Invoke(null, new object[] { handler })!; + } + + private static Func CreateAsyncInvoker(Type eventType, Delegate handler) + { + var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateAsyncInvokerGeneric), + BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType); + return (Func)factory.Invoke(null, new object[] { handler })!; + } + + private static Action CreateSyncInvokerGeneric(Delegate handler) where TEvent : EventBase + { + var typedHandler = (Action)handler; + return eventArgs => typedHandler((TEvent)eventArgs); + } + + private static Func CreateAsyncInvokerGeneric(Delegate handler) + where TEvent : EventBase + { + var typedHandler = (Func)handler; + return eventArgs => typedHandler((TEvent)eventArgs); + } } -} \ No newline at end of file +} diff --git a/Runtime/EventPool.cs b/Runtime/EventPool.cs index 8f8eed2..fab4bb1 100644 --- a/Runtime/EventPool.cs +++ b/Runtime/EventPool.cs @@ -1,10 +1,12 @@ -using System; +using System; using System.Collections.Generic; namespace ShrinkEventBus { public static class EventPool where T : EventBase, new() { + private const int MaxPoolSize = 128; + private static readonly Stack Pool = new(32); private static readonly object Lock = new(); private static readonly Action CachedReleaseAction = e => Release((T)e); @@ -28,15 +30,18 @@ namespace ShrinkEventBus public static void Release(T evt) { - if (evt == null || evt.IsInPool) return; - - evt.ResetInternal(); - evt.IsInPool = true; + if (evt == null) + return; lock (Lock) { + if (evt.IsInPool || Pool.Count >= MaxPoolSize) + return; + + evt.ResetInternal(); + evt.IsInPool = true; Pool.Push(evt); } } } -} \ No newline at end of file +} diff --git a/Runtime/EventPriority.cs b/Runtime/EventPriority.cs index 43b766f..852c5e7 100644 --- a/Runtime/EventPriority.cs +++ b/Runtime/EventPriority.cs @@ -18,7 +18,7 @@ { >= 100 => EventPriority.HIGHEST, >= 50 => EventPriority.HIGH, - > 0 => EventPriority.NORMAL, + >= 0 => EventPriority.NORMAL, >= -50 => EventPriority.LOW, _ => EventPriority.LOWEST }; diff --git a/Runtime/IShrinkEventBus.cs b/Runtime/IShrinkEventBus.cs new file mode 100644 index 0000000..06bbb6e --- /dev/null +++ b/Runtime/IShrinkEventBus.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Cysharp.Threading.Tasks; + +namespace ShrinkEventBus +{ + public interface IShrinkEventSubscription : IDisposable + { + long SubscriptionId { get; } + bool IsDisposed { get; } + } + + public readonly struct ShrinkEventSubscriptionSnapshot + { + public ShrinkEventSubscriptionSnapshot(long subscriptionId, Type eventType, EventPriority priority, + int numericPriority, bool receiveCanceled, object target, string targetTypeName, string methodName, + string debugInfo, DateTime registeredAtUtc) + { + SubscriptionId = subscriptionId; + EventType = eventType ?? throw new ArgumentNullException(nameof(eventType)); + Priority = priority; + NumericPriority = numericPriority; + ReceiveCanceled = receiveCanceled; + Target = target; + TargetTypeName = targetTypeName ?? string.Empty; + MethodName = methodName ?? string.Empty; + DebugInfo = debugInfo ?? string.Empty; + RegisteredAtUtc = registeredAtUtc; + } + + public long SubscriptionId { get; } + public Type EventType { get; } + public EventPriority Priority { get; } + public int NumericPriority { get; } + public bool ReceiveCanceled { get; } + public object Target { get; } + public string TargetTypeName { get; } + public string MethodName { get; } + public string DebugInfo { get; } + public DateTime RegisteredAtUtc { get; } + public bool IsStaticHandler => Target == null; + } + + public interface IShrinkEventBus + { + event Action OnEventTriggered; + +#if UNITY_EDITOR + event Action OnEventTriggeredForEditor; + bool EnableDebugRecord { get; set; } +#endif + + bool IsStarted { get; } + + void Start(); + void AutoRegister(object target); + void Register(object target); + void Unregister(object target); + + void RegisterEvent(Action handler, EventPriority priority = EventPriority.NORMAL, + bool receiveCanceled = false) where TEvent : EventBase; + void RegisterEvent(Action handler, int priority) where TEvent : EventBase; + void RegisterEvent(Func handler, EventPriority priority = EventPriority.NORMAL, + bool receiveCanceled = false) where TEvent : EventBase; + void RegisterEvent(Func handler, int priority) where TEvent : EventBase; + IShrinkEventSubscription SubscribeEvent(Action handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase; + IShrinkEventSubscription SubscribeEvent(Action handler, int priority) + where TEvent : EventBase; + IShrinkEventSubscription SubscribeEvent(Func handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase; + IShrinkEventSubscription SubscribeEvent(Func handler, int priority) + where TEvent : EventBase; + + void UnregisterEvent(Action handler) where TEvent : EventBase; + void UnregisterEvent(Func handler) where TEvent : EventBase; + void ClearAllSubscribersForEvent() where TEvent : EventBase; + void UnregisterAllEventsForObject(object targetObject); + void UnregisterAllEvents(); + + bool TriggerEvent(TEvent eventArgs) where TEvent : EventBase; + bool TriggerEvent(EventPriority phase, TEvent eventArgs) where TEvent : EventBase; + UniTask TriggerEventAsync(TEvent eventArgs) where TEvent : EventBase; + UniTask TriggerEventAsync(EventPriority phase, TEvent eventArgs) where TEvent : EventBase; + + EventHandlerInfo[] GetEventSubscribers() where TEvent : EventBase; + ListenerList GetListenerList() where TEvent : EventBase; + IReadOnlyDictionary GetAllSubscribersSnapshot(); + IReadOnlyList GetActiveSubscriptionsSnapshot(); + + bool IsInstanceRegistered(object target); + int GetRegisteredInstanceCount(); + int GetRegisteredEventTypeCount(); + } + + public interface IShrinkEventExceptionHandler + { + void HandleException(IShrinkEventBus bus, EventBase eventArgs, EventHandlerInfo[] listeners, int index, + Exception exception); + } + + public enum ShrinkEventExceptionHandlingMode + { + LogAndContinue, + Throw, + LogAndThrow + } +} diff --git a/Runtime/IShrinkEventBus.cs.meta b/Runtime/IShrinkEventBus.cs.meta new file mode 100644 index 0000000..a95c735 --- /dev/null +++ b/Runtime/IShrinkEventBus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb69f308f5c727542ac602709b02c628 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ListenerList.cs b/Runtime/ListenerList.cs index 4823ffa..c214786 100644 --- a/Runtime/ListenerList.cs +++ b/Runtime/ListenerList.cs @@ -1,50 +1,84 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; -using System.Reflection; namespace ShrinkEventBus { public class ListenerList { - private readonly object _lock = new(); - private readonly List _list = new(); + private static readonly EventPriority[] Priorities = (EventPriority[])Enum.GetValues(typeof(EventPriority)); + + private readonly object _lock; + private readonly List[] _priorityBuckets; + private readonly ListenerList? _parent; + private List? _children; private EventHandlerInfo[] _snapshot = Array.Empty(); + private readonly EventHandlerInfo[]?[] _phaseSnapshots = new EventHandlerInfo[Priorities.Length][]; private bool _dirty; - public void Add(Delegate handler, EventPriority priority, int numericPriority, - bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null) + public ListenerList(ListenerList? parent = null) : this(parent, null) { - var newInfo = new EventHandlerInfo(handler, priority, numericPriority, - receiveCanceled, debugInfo, originalMethod); - AddInfo(newInfo); } + // 同一条父子链必须共用一把锁,否则脏标记传播与快照重建会产生竞态 + internal ListenerList(ListenerList? parent, object? sharedLock) + { + _lock = sharedLock ?? parent?._lock ?? new object(); + _priorityBuckets = new List[Priorities.Length]; + for (var i = 0; i < _priorityBuckets.Length; i++) + _priorityBuckets[i] = new List(); + + _parent = parent; + _parent?.AddChild(this); + _dirty = true; + } + + public int LocalCount + { + get + { + lock (_lock) + { + var count = 0; + foreach (var list in _priorityBuckets) + count += list.Count; + return count; + } + } + } + + public int Count => GetHandlers().Length; + public void Add(EventHandlerInfo info) { - AddInfo(info); - } + if (info == null) + throw new ArgumentNullException(nameof(info)); - private void AddInfo(EventHandlerInfo info) - { lock (_lock) { - var index = BinarySearchInsertIndex(info.Priority, info.NumericPriority); - _list.Insert(index, info); - _dirty = true; + var list = _priorityBuckets[(int)info.Priority]; + var index = BinarySearchInsertIndex(list, info.NumericPriority); + list.Insert(index, info); + MarkDirty(); } } public bool Remove(Delegate handler) { + if (handler == null) + return false; + lock (_lock) { - for (var i = 0; i < _list.Count; i++) + foreach (var list in _priorityBuckets) { - if (_list[i].Handler.Equals(handler)) + for (var i = 0; i < list.Count; i++) { - _list.RemoveAt(i); - _dirty = true; + if (!list[i].Handler.Equals(handler)) + continue; + + list.RemoveAt(i); + MarkDirty(); return true; } } @@ -53,21 +87,55 @@ namespace ShrinkEventBus } } + public int RemoveWhere(Predicate predicate) + { + if (predicate == null) + return 0; + + lock (_lock) + { + var removedCount = 0; + foreach (var list in _priorityBuckets) + { + for (var i = list.Count - 1; i >= 0; i--) + { + if (!predicate(list[i])) + continue; + + list.RemoveAt(i); + removedCount++; + } + } + + if (removedCount > 0) + MarkDirty(); + + return removedCount; + } + } + public void RemoveTarget(object target) { + if (target == null) + return; + lock (_lock) { var removed = false; - for (var i = _list.Count - 1; i >= 0; i--) + foreach (var list in _priorityBuckets) { - if (_list[i].Target == target) + for (var i = list.Count - 1; i >= 0; i--) { - _list.RemoveAt(i); + if (!ReferenceEquals(list[i].Target, target)) + continue; + + list.RemoveAt(i); removed = true; } } - if (removed) _dirty = true; + if (removed) + MarkDirty(); } } @@ -76,20 +144,20 @@ namespace ShrinkEventBus lock (_lock) { if (_dirty) - { - _snapshot = _list.ToArray(); - _dirty = false; - } + RebuildSnapshot(); return _snapshot; } } - public int Count + public EventHandlerInfo[] GetHandlers(EventPriority priority) { - get + lock (_lock) { - lock (_lock) return _list.Count; + if (_dirty) + RebuildSnapshot(); + + return _phaseSnapshots[(int)priority] ?? Array.Empty(); } } @@ -97,33 +165,98 @@ namespace ShrinkEventBus { lock (_lock) { - _list.Clear(); - _snapshot = Array.Empty(); - _dirty = false; + foreach (var list in _priorityBuckets) + list.Clear(); + MarkDirty(); } } - private int BinarySearchInsertIndex(EventPriority priority, int numericPriority) + private void AddChild(ListenerList child) + { + if (child == null) + return; + + lock (_lock) + { + _children ??= new List(2); + _children.Add(child); + } + } + + private void MarkDirty() + { + _dirty = true; + _snapshot = Array.Empty(); + Array.Clear(_phaseSnapshots, 0, _phaseSnapshots.Length); + + if (_children == null) + return; + + for (var i = 0; i < _children.Count; i++) + _children[i].MarkDirty(); + } + + private void RebuildSnapshot() + { + var merged = new List(); + + for (var i = 0; i < Priorities.Length; i++) + { + var parentHandlers = _parent != null + ? _parent.GetHandlers(Priorities[i]) + : Array.Empty(); + + var phaseSnapshot = MergeByNumericPriority(_priorityBuckets[i], parentHandlers); + _phaseSnapshots[i] = phaseSnapshot; + merged.AddRange(phaseSnapshot); + } + + _snapshot = merged.ToArray(); + _dirty = false; + } + + // 两侧均已按 NumericPriority 降序排列;平局时本类型 handler 在前 + private static EventHandlerInfo[] MergeByNumericPriority(List own, + EventHandlerInfo[] parentHandlers) + { + if (parentHandlers.Length == 0) + return own.ToArray(); + if (own.Count == 0) + return parentHandlers; + + var result = new EventHandlerInfo[own.Count + parentHandlers.Length]; + int i = 0, j = 0, k = 0; + while (i < own.Count && j < parentHandlers.Length) + { + result[k++] = parentHandlers[j].NumericPriority > own[i].NumericPriority + ? parentHandlers[j++] + : own[i++]; + } + + while (i < own.Count) + result[k++] = own[i++]; + while (j < parentHandlers.Length) + result[k++] = parentHandlers[j++]; + + return result; + } + + private static int BinarySearchInsertIndex(List list, int numericPriority) { var lo = 0; - var hi = _list.Count; + var hi = list.Count; while (lo < hi) { var mid = (lo + hi) >> 1; - var existing = _list[mid]; - var cmp = existing.Priority.CompareTo(priority); - - if (cmp == 0) - cmp = numericPriority.CompareTo(existing.NumericPriority); - - if (cmp < 0) - lo = mid + 1; - else + var cmp = numericPriority.CompareTo(list[mid].NumericPriority); + if (cmp > 0) hi = mid; + else + lo = mid + 1; } return lo; } } -} \ No newline at end of file +} diff --git a/Runtime/ShrinkEventBusBuilder.cs b/Runtime/ShrinkEventBusBuilder.cs new file mode 100644 index 0000000..09f1cc3 --- /dev/null +++ b/Runtime/ShrinkEventBusBuilder.cs @@ -0,0 +1,76 @@ +#nullable enable +using System; + +namespace ShrinkEventBus +{ + public sealed class ShrinkEventBusBuilder + { + internal IShrinkEventExceptionHandler? ExceptionHandler { get; private set; } + internal ShrinkEventExceptionHandlingMode ExceptionHandlingMode { get; private set; } = + ShrinkEventExceptionHandlingMode.LogAndContinue; + internal Action? EventClassChecker { get; private set; } + internal bool StartShutdownEnabled { get; private set; } + internal bool CheckTypesOnDispatchEnabled { get; private set; } + internal bool AllowPerPhaseDispatchEnabled { get; private set; } + + public ShrinkEventBusBuilder SetExceptionHandler(IShrinkEventExceptionHandler handler) + { + ExceptionHandler = handler ?? throw new ArgumentNullException(nameof(handler)); + return this; + } + + public ShrinkEventBusBuilder SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode mode) + { + ExceptionHandlingMode = mode; + return this; + } + + public ShrinkEventBusBuilder StartShutdown() + { + StartShutdownEnabled = true; + return this; + } + + public ShrinkEventBusBuilder CheckTypesOnDispatch() + { + CheckTypesOnDispatchEnabled = true; + return this; + } + + public ShrinkEventBusBuilder AllowPerPhaseDispatch() + { + AllowPerPhaseDispatchEnabled = true; + return this; + } + + public ShrinkEventBusBuilder ClassChecker(Action checker) + { + if (checker == null) + throw new ArgumentNullException(nameof(checker)); + + EventClassChecker = EventClassChecker == null + ? checker + : EventClassChecker + checker; + return this; + } + + public ShrinkEventBusBuilder MarkerInterface() where TMarker : class + { + var markerType = typeof(TMarker); + if (!markerType.IsInterface) + throw new InvalidOperationException($"Marker type {markerType.FullName} must be an interface."); + + return ClassChecker(eventType => + { + if (!markerType.IsAssignableFrom(eventType)) + throw new ArgumentException( + $"This bus only accepts events assignable to {markerType.FullName}, but got {eventType.FullName}."); + }); + } + + public IShrinkEventBus Build() + { + return new ShrinkEventBusInstance(this); + } + } +} diff --git a/Runtime/ShrinkEventBusBuilder.cs.meta b/Runtime/ShrinkEventBusBuilder.cs.meta new file mode 100644 index 0000000..97c4091 --- /dev/null +++ b/Runtime/ShrinkEventBusBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aad8b06ecb53b8a4998fd9944673e537 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkEventBusInstance.cs b/Runtime/ShrinkEventBusInstance.cs new file mode 100644 index 0000000..8dbdac2 --- /dev/null +++ b/Runtime/ShrinkEventBusInstance.cs @@ -0,0 +1,708 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace ShrinkEventBus +{ + internal sealed class ShrinkEventBusInstance : IShrinkEventBus + { + private sealed class EventSubscription : IShrinkEventSubscription + { + private readonly ShrinkEventBusInstance _owner; + private bool _disposed; + + public EventSubscription(ShrinkEventBusInstance owner, long subscriptionId) + { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); + SubscriptionId = subscriptionId; + } + + public long SubscriptionId { get; } + public bool IsDisposed => _disposed; + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _owner.UnregisterSubscription(SubscriptionId); + } + } + + private readonly Dictionary _eventHandlers = new(); + private readonly HashSet _registeredTargets = new(); + private readonly object _listenerLock = new(); + private readonly object _registrationLock = new(); + private long _nextSubscriptionId; + private readonly IShrinkEventExceptionHandler? _exceptionHandler; + private readonly ShrinkEventExceptionHandlingMode _exceptionHandlingMode; + private readonly Action? _eventClassChecker; + private readonly bool _checkTypesOnDispatch; + private readonly bool _allowPerPhaseDispatch; + + public event Action? OnEventTriggered; + +#if UNITY_EDITOR + public bool EnableDebugRecord { get; set; } + public event Action? OnEventTriggeredForEditor; +#endif + + public bool IsStarted { get; private set; } + + public ShrinkEventBusInstance(ShrinkEventBusBuilder builder) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + _exceptionHandler = builder.ExceptionHandler; + _exceptionHandlingMode = builder.ExceptionHandlingMode; + _eventClassChecker = builder.EventClassChecker; + _checkTypesOnDispatch = builder.CheckTypesOnDispatchEnabled; + _allowPerPhaseDispatch = builder.AllowPerPhaseDispatchEnabled; + IsStarted = !builder.StartShutdownEnabled; + } + + public void Start() + { + IsStarted = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AutoRegister(object target) + { + RegisterCore(target, requireSubscriberAttribute: true, lenientWhenNoMethods: true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Register(object target) + { + RegisterCore(target, requireSubscriberAttribute: false, lenientWhenNoMethods: false); + } + + public void Unregister(object target) + { + if (target == null) + return; + + lock (_registrationLock) + { + _registeredTargets.Remove(target); + } + + switch (target) + { + case Delegate handler: + RemoveHandlers(info => Equals(info.Handler, handler)); + return; + case MethodInfo method: + RemoveHandlers(info => info.MatchesMethod(method)); + return; + case Type declaringType: + RemoveHandlers(info => info.Target == null && info.MatchesDeclaringType(declaringType)); + return; + default: + UnregisterAllEventsForObject(target); + return; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterEvent(Func handler, int priority) + where TEvent : EventBase + { + var eventPriority = PriorityHelper.ConvertToEventPriority(priority); + RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false, + $"Manual Async Handler (Priority: {priority})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterEvent(Action handler, EventPriority priority = EventPriority.NORMAL, + bool receiveCanceled = false) where TEvent : EventBase + { + RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled, + $"Manual Sync Handler (Priority: {priority})", handler.Method); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterEvent(Action handler, int priority) where TEvent : EventBase + { + var eventPriority = PriorityHelper.ConvertToEventPriority(priority); + RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false, + $"Manual Sync Handler (Priority: {priority})", handler.Method); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterEvent(Func handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase + { + RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled, + $"Manual Async Handler (Priority: {priority})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IShrinkEventSubscription SubscribeEvent(Action handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase + { + return SubscribeEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled, + $"Manual Sync Subscription (Priority: {priority})", handler.Method); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IShrinkEventSubscription SubscribeEvent(Action handler, int priority) + where TEvent : EventBase + { + var eventPriority = PriorityHelper.ConvertToEventPriority(priority); + return SubscribeEventInternal(typeof(TEvent), handler, eventPriority, priority, false, + $"Manual Sync Subscription (Priority: {priority})", handler.Method); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IShrinkEventSubscription SubscribeEvent(Func handler, + EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase + { + return SubscribeEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled, + $"Manual Async Subscription (Priority: {priority})"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IShrinkEventSubscription SubscribeEvent(Func handler, int priority) + where TEvent : EventBase + { + var eventPriority = PriorityHelper.ConvertToEventPriority(priority); + return SubscribeEventInternal(typeof(TEvent), handler, eventPriority, priority, false, + $"Manual Async Subscription (Priority: {priority})"); + } + + internal void RegisterEventInternal(Type eventType, Delegate handler, EventPriority priority, + int numericPriority, bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null) + { + if (eventType == null) + throw new ArgumentNullException(nameof(eventType)); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + ValidateEventType(eventType, "register"); + var handlerInfo = EventHandlerInfo.Create(GetNextSubscriptionId(), handler, eventType, priority, + numericPriority, receiveCanceled, + debugInfo, originalMethod); + + lock (_listenerLock) + { + GetOrCreateListenerList(eventType).Add(handlerInfo); + } + } + + private IShrinkEventSubscription SubscribeEventInternal(Type eventType, Delegate handler, EventPriority priority, + int numericPriority, bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null) + { + if (eventType == null) + throw new ArgumentNullException(nameof(eventType)); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + ValidateEventType(eventType, "subscribe"); + var subscriptionId = GetNextSubscriptionId(); + var handlerInfo = EventHandlerInfo.Create(subscriptionId, handler, eventType, priority, numericPriority, + receiveCanceled, debugInfo, originalMethod); + + lock (_listenerLock) + { + GetOrCreateListenerList(eventType).Add(handlerInfo); + } + + return new EventSubscription(this, subscriptionId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UnregisterEvent(Func handler) where TEvent : EventBase + { + if (handler == null) + return; + + var collection = TryGetListenerList(typeof(TEvent)); + collection?.Remove(handler); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UnregisterEvent(Action handler) where TEvent : EventBase + { + if (handler == null) + return; + + var collection = TryGetListenerList(typeof(TEvent)); + collection?.Remove(handler); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearAllSubscribersForEvent() where TEvent : EventBase + { + var collection = TryGetListenerList(typeof(TEvent)); + collection?.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UnregisterAllEventsForObject(object targetObject) + { + if (targetObject is null) + return; + + lock (_listenerLock) + { + foreach (var kvp in _eventHandlers) + kvp.Value.RemoveTarget(targetObject); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UnregisterAllEvents() + { + lock (_listenerLock) + { + foreach (var kvp in _eventHandlers) + kvp.Value.Clear(); + } + + lock (_registrationLock) + { + _registeredTargets.Clear(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UniTask TriggerEventAsync(TEvent eventArgs) where TEvent : EventBase + { + return TriggerEventAsyncInternal(eventArgs, null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UniTask TriggerEventAsync(EventPriority phase, TEvent eventArgs) where TEvent : EventBase + { + EnsurePerPhaseDispatchAllowed(); + return TriggerEventAsyncInternal(eventArgs, phase); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TriggerEvent(TEvent eventArgs) where TEvent : EventBase + { + return TriggerEventInternal(eventArgs, null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TriggerEvent(EventPriority phase, TEvent eventArgs) where TEvent : EventBase + { + EnsurePerPhaseDispatchAllowed(); + return TriggerEventInternal(eventArgs, phase); + } + + private async UniTask TriggerEventAsyncInternal(EventBase eventArgs, EventPriority? phase) + { + if (!TryPrepareDispatch(eventArgs, phase, out var eventType, out var handlers)) + return false; + + var wasHandled = false; + for (var i = 0; i < handlers.Length; i++) + { + var handlerInfo = handlers[i]; + if (!TryPrepareHandler(eventArgs, handlerInfo)) + continue; + + try + { + if (handlerInfo.SyncInvoker != null) + { + handlerInfo.SyncInvoker(eventArgs); + wasHandled = true; + } + else if (handlerInfo.AsyncInvoker != null) + { + await handlerInfo.AsyncInvoker(eventArgs); + wasHandled = true; + } + } + catch (Exception ex) + { + if (HandleListenerException(eventArgs, handlers, i, ex)) + throw; + } + } + + eventArgs.CurrentHandler = null; + CompleteEventDispatch(eventArgs, eventType, handlers); + return wasHandled; + } + + private bool TriggerEventInternal(EventBase eventArgs, EventPriority? phase) + { + if (!TryPrepareDispatch(eventArgs, phase, out var eventType, out var handlers)) + return false; + + var wasHandled = false; + for (var i = 0; i < handlers.Length; i++) + { + var handlerInfo = handlers[i]; + if (!TryPrepareHandler(eventArgs, handlerInfo)) + continue; + + try + { + if (handlerInfo.SyncInvoker != null) + { + handlerInfo.SyncInvoker(eventArgs); + wasHandled = true; + } + else if (handlerInfo.AsyncInvoker != null) + { + var detachedEvent = EventCloneUtility.CloneForDetachedDispatch(eventArgs); + FireAndForgetSafe(() => handlerInfo.AsyncInvoker(detachedEvent), + $"{handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName}"); + wasHandled = true; + } + } + catch (Exception ex) + { + if (HandleListenerException(eventArgs, handlers, i, ex)) + throw; + } + } + + eventArgs.CurrentHandler = null; + CompleteEventDispatch(eventArgs, eventType, handlers); + return wasHandled; + } + + private bool TryPrepareDispatch(EventBase eventArgs, EventPriority? phase, out Type eventType, + out EventHandlerInfo[] handlers) + { + if (eventArgs == null) + throw new ArgumentNullException(nameof(eventArgs)); + + if (!IsStarted) + { + eventType = eventArgs.GetType(); + handlers = Array.Empty(); + return false; + } + + eventType = eventArgs.GetType(); + ValidateDispatchEventType(eventType); + eventArgs.PrepareForDispatch(); + + lock (_listenerLock) + { + var listenerList = GetOrCreateListenerList(eventType); + handlers = phase.HasValue ? listenerList.GetHandlers(phase.Value) : listenerList.GetHandlers(); + } + + eventArgs.SetListenerSnapshot(handlers); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryPrepareHandler(EventBase eventArgs, EventHandlerInfo handlerInfo) + { + eventArgs.CurrentHandler = handlerInfo; + eventArgs.SetPhase(handlerInfo.Priority); + if (eventArgs.IsCancelable && eventArgs.IsCanceled && !handlerInfo.ReceiveCanceled) + return false; + return true; + } + + private bool HandleListenerException(EventBase eventArgs, EventHandlerInfo[] handlers, int index, Exception exception) + { + _exceptionHandler?.HandleException(this, eventArgs, handlers, index, exception); + + switch (_exceptionHandlingMode) + { + case ShrinkEventExceptionHandlingMode.Throw: + return true; + case ShrinkEventExceptionHandlingMode.LogAndThrow: + LogHandlerException(exception); + return true; + default: + LogHandlerException(exception); + return false; + } + } + + private static void LogHandlerException(Exception exception) + { + Debug.LogException(exception); + Debug.LogError($"[EventBus] Handler exception: {exception.Message}"); + } + + private static void FireAndForgetSafe(Func action, string handlerName) + { + action().Forget(e => + { + Debug.LogException(e); + Debug.LogError($"[EventBus] Async Handler {handlerName} threw exception: {e.Message}"); + }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CompleteEventDispatch(EventBase eventArgs, Type eventType, EventHandlerInfo[] handlers) + { + OnEventTriggered?.Invoke(eventArgs, eventType); + +#if UNITY_EDITOR + var editorTraceHandler = OnEventTriggeredForEditor; + if (!EnableDebugRecord || editorTraceHandler == null) + return; + + editorTraceHandler.Invoke(eventArgs, eventType.Name, GetSenderInfo(), handlers); +#endif + } + +#if UNITY_EDITOR + private static string GetSenderInfo() + { + var senderInfo = "Unknown"; + try + { + var trace = new System.Diagnostics.StackTrace(0, false); + for (var i = 0; i < trace.FrameCount; i++) + { + var method = trace.GetFrame(i)?.GetMethod(); + var declaringType = method?.DeclaringType; + if (declaringType == null) + continue; + + if (declaringType == typeof(EventBus) || declaringType.DeclaringType == typeof(EventBus)) + continue; + + if (declaringType == typeof(ShrinkEventBusInstance) || + declaringType.DeclaringType == typeof(ShrinkEventBusInstance)) + continue; + + var ns = declaringType.Namespace ?? string.Empty; + if (ns.StartsWith("System") || ns.StartsWith("Cysharp") || ns.StartsWith("UnityEngine")) + continue; + + var className = declaringType.Name; + var methodName = method!.Name; + if (declaringType.DeclaringType != null && className.StartsWith("<") && className.Contains(">")) + { + className = declaringType.DeclaringType.Name; + var startIndex = declaringType.Name.IndexOf('<') + 1; + var endIndex = declaringType.Name.IndexOf('>'); + if (startIndex > 0 && endIndex > startIndex) + methodName = declaringType.Name.Substring(startIndex, endIndex - startIndex); + } + + return $"{className}.{methodName}()"; + } + } + catch + { + } + + return senderInfo; + } +#endif + + public EventHandlerInfo[] GetEventSubscribers() where TEvent : EventBase + { + var listenerList = TryGetListenerList(typeof(TEvent)); + var handlers = listenerList?.GetHandlers(); + if (handlers == null || handlers.Length == 0) + return Array.Empty(); + + return (EventHandlerInfo[])handlers.Clone(); + } + + public ListenerList GetListenerList() where TEvent : EventBase + { + lock (_listenerLock) + { + return GetOrCreateListenerList(typeof(TEvent)); + } + } + + public IReadOnlyDictionary GetAllSubscribersSnapshot() + { + lock (_listenerLock) + { + var snapshot = new Dictionary(_eventHandlers.Count); + foreach (var kvp in _eventHandlers) + { + var handlers = kvp.Value.GetHandlers(); + if (handlers.Length == 0) + continue; + + snapshot[kvp.Key] = (EventHandlerInfo[])handlers.Clone(); + } + + return snapshot; + } + } + + public IReadOnlyList GetActiveSubscriptionsSnapshot() + { + lock (_listenerLock) + { + var snapshot = new List(); + foreach (var entry in _eventHandlers) + { + var eventType = entry.Key; + var handlers = entry.Value.GetHandlers(); + for (var i = 0; i < handlers.Length; i++) + { + var handler = handlers[i]; + snapshot.Add(new ShrinkEventSubscriptionSnapshot( + handler.SubscriptionId, + eventType, + handler.Priority, + handler.NumericPriority, + handler.ReceiveCanceled, + handler.Target, + handler.DisplayDeclaringType.FullName ?? handler.DisplayDeclaringType.Name, + handler.DisplayMethodName, + handler.DebugInfo, + handler.RegisteredAtUtc)); + } + } + + return snapshot; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsInstanceRegistered(object target) + { + if (target == null) + return false; + + lock (_registrationLock) + { + return _registeredTargets.Contains(target); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetRegisteredInstanceCount() + { + lock (_registrationLock) + { + return _registeredTargets.Count; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetRegisteredEventTypeCount() + { + lock (_listenerLock) + { + var count = 0; + foreach (var listenerList in _eventHandlers.Values) + { + if (listenerList.LocalCount > 0) + count++; + } + + return count; + } + } + + private void RegisterCore(object target, bool requireSubscriberAttribute, bool lenientWhenNoMethods) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + lock (_registrationLock) + { + if (_registeredTargets.Contains(target)) + return; + + EventBusRegHelper.RegisterTarget(this, target, requireSubscriberAttribute, lenientWhenNoMethods); + _registeredTargets.Add(target); + } + } + + private void RemoveHandlers(Predicate predicate) + { + lock (_listenerLock) + { + foreach (var listenerList in _eventHandlers.Values) + listenerList.RemoveWhere(predicate); + } + } + + private void UnregisterSubscription(long subscriptionId) + { + RemoveHandlers(info => info.SubscriptionId == subscriptionId); + } + + private long GetNextSubscriptionId() => Interlocked.Increment(ref _nextSubscriptionId); + + private ListenerList GetOrCreateListenerList(Type eventType) + { + if (_eventHandlers.TryGetValue(eventType, out var existing)) + return existing; + + ListenerList? parent = null; + var parentType = GetParentEventType(eventType); + if (parentType != null) + parent = GetOrCreateListenerList(parentType); + + var created = new ListenerList(parent, _listenerLock); + _eventHandlers[eventType] = created; + return created; + } + + private ListenerList? TryGetListenerList(Type eventType) + { + if (eventType == null) + return null; + + lock (_listenerLock) + { + return _eventHandlers.TryGetValue(eventType, out var listenerList) ? listenerList : null; + } + } + + private void ValidateEventType(Type eventType, string action) + { + if (!typeof(EventBase).IsAssignableFrom(eventType)) + throw new ArgumentException( + $"Cannot {action} listener for {eventType.FullName}: type must inherit from EventBase."); + + _eventClassChecker?.Invoke(eventType); + } + + private void ValidateDispatchEventType(Type eventType) + { + if (!typeof(EventBase).IsAssignableFrom(eventType)) + throw new ArgumentException($"Cannot dispatch event type {eventType.FullName}: not an EventBase."); + + if (_checkTypesOnDispatch) + _eventClassChecker?.Invoke(eventType); + } + + private static Type? GetParentEventType(Type eventType) + { + var current = eventType.BaseType; + while (current != null && current != typeof(object)) + { + if (typeof(EventBase).IsAssignableFrom(current)) + return current; + + current = current.BaseType; + } + + return null; + } + + private void EnsurePerPhaseDispatchAllowed() + { + if (!_allowPerPhaseDispatch) + throw new InvalidOperationException( + "Per-phase event dispatch is disabled for this bus. Enable it via ShrinkEventBusBuilder.AllowPerPhaseDispatch()."); + } + } +} diff --git a/Runtime/ShrinkEventBusInstance.cs.meta b/Runtime/ShrinkEventBusInstance.cs.meta new file mode 100644 index 0000000..0a5ddaf --- /dev/null +++ b/Runtime/ShrinkEventBusInstance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 92c1c7cd96e37c8458600728c7c0c57f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..1615325 --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f28409a6ba08444cbdbfc5eb37b1809 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/EventBusDispatchTests.cs b/Tests/EventBusDispatchTests.cs new file mode 100644 index 0000000..b3c5984 --- /dev/null +++ b/Tests/EventBusDispatchTests.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using NUnit.Framework; + +namespace ShrinkEventBus.Tests +{ + [TestFixture] + public sealed class EventBusDispatchTests + { + private class PlainEvent : EventBase + { + } + + private sealed class DerivedPlainEvent : PlainEvent + { + } + + [Cancelable] + private sealed class CancelableEvent : EventBase + { + } + + [HasResult] + private sealed class ResultEvent : EventBase + { + } + + [Test] + public void TriggerEvent_ReturnsFalseWithoutSubscribers_TrueWhenHandled() + { + var bus = EventBus.CreateBus(); + + Assert.IsFalse(bus.TriggerEvent(new PlainEvent())); + + bus.RegisterEvent(_ => { }, EventPriority.NORMAL); + Assert.IsTrue(bus.TriggerEvent(new PlainEvent())); + } + + [Test] + public void CanceledEvent_SkipsHandlersUnlessReceiveCanceled() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + bus.RegisterEvent(evt => + { + order.Add("canceler"); + evt.SetCanceled(true); + }, EventPriority.HIGHEST); + bus.RegisterEvent(_ => order.Add("skipped"), EventPriority.NORMAL); + bus.RegisterEvent(evt => order.Add($"monitor:{evt.IsCanceled}"), + EventPriority.MONITOR, receiveCanceled: true); + + var evt = new CancelableEvent(); + bus.TriggerEvent(evt); + + Assert.IsTrue(evt.IsCanceled); + CollectionAssert.AreEqual(new[] { "canceler", "monitor:True" }, order); + } + + [Test] + public void SetCanceled_OnNonCancelableEvent_ThrowsWithMessage() + { + var evt = new PlainEvent(); + + var ex = Assert.Throws(() => evt.SetCanceled(true)); + Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message)); + } + + [Test] + public void SetResult_OnEventWithoutResult_ThrowsWithMessage() + { + var evt = new PlainEvent(); + + var ex = Assert.Throws(() => evt.SetResult(EventResult.ALLOW)); + Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message)); + } + + [Test] + public void SetResult_OnResultEvent_PersistsResult() + { + var bus = EventBus.CreateBus(); + bus.RegisterEvent(evt => evt.SetResult(EventResult.DENY), EventPriority.HIGH); + + var evt = new ResultEvent(); + bus.TriggerEvent(evt); + + Assert.AreEqual(EventResult.DENY, evt.Result); + } + + [Test] + public void ParentSubscriber_ReceivesDerivedEvent() + { + var bus = EventBus.CreateBus(); + var hits = 0; + bus.RegisterEvent(_ => hits++, EventPriority.NORMAL); + + bus.TriggerEvent(new DerivedPlainEvent()); + + Assert.AreEqual(1, hits); + } + + [Test] + public void ParentSubscriber_RegisteredAfterChildWasTriggered_StillReceivesChild() + { + var bus = EventBus.CreateBus(); + + // 先触发子事件,让子类型的监听列表先于父监听器创建(覆盖脏传播路径) + bus.TriggerEvent(new DerivedPlainEvent()); + + var hits = 0; + bus.RegisterEvent(_ => hits++, EventPriority.NORMAL); + bus.TriggerEvent(new DerivedPlainEvent()); + + Assert.AreEqual(1, hits); + } + + [Test] + public void PerPhaseDispatch_OnlyRunsRequestedPhase() + { + var bus = EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch()); + var order = new List(); + + bus.RegisterEvent(_ => order.Add("high"), EventPriority.HIGH); + bus.RegisterEvent(_ => order.Add("normal"), EventPriority.NORMAL); + + bus.TriggerEvent(EventPriority.HIGH, new PlainEvent()); + + CollectionAssert.AreEqual(new[] { "high" }, order); + } + + [Test] + public void PerPhaseDispatch_ThrowsWhenNotEnabled() + { + var bus = EventBus.CreateBus(); + + Assert.Throws(() => bus.TriggerEvent(EventPriority.HIGH, new PlainEvent())); + } + + [Test] + public void TriggerEventAsync_RunsHandlersSequentiallyByPriority() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + async UniTask AsyncHandler(PlainEvent evt) + { + order.Add("async-high"); + await UniTask.CompletedTask; + } + + bus.RegisterEvent(AsyncHandler, EventPriority.HIGH, receiveCanceled: false); + bus.RegisterEvent(_ => order.Add("sync-normal"), EventPriority.NORMAL); + + var handled = bus.TriggerEventAsync(new PlainEvent()).GetAwaiter().GetResult(); + + Assert.IsTrue(handled); + CollectionAssert.AreEqual(new[] { "async-high", "sync-normal" }, order); + } + + [Test] + public void TriggerEventAsync_AsyncHandlerCancellation_SkipsLaterHandlers() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + async UniTask CancelingHandler(CancelableEvent evt) + { + evt.SetCanceled(true); + order.Add("async-canceler"); + await UniTask.CompletedTask; + } + + bus.RegisterEvent(CancelingHandler, EventPriority.HIGHEST, receiveCanceled: false); + bus.RegisterEvent(_ => order.Add("skipped"), EventPriority.NORMAL); + + bus.TriggerEventAsync(new CancelableEvent()).GetAwaiter().GetResult(); + + CollectionAssert.AreEqual(new[] { "async-canceler" }, order); + } + + [Test] + public void GetSubscribers_ReturnsDefensiveCopyOfDispatchSnapshot() + { + var bus = EventBus.CreateBus(); + bus.RegisterEvent(_ => { }, EventPriority.NORMAL); + + var evt = new PlainEvent(); + bus.TriggerEvent(evt); + + var first = evt.GetSubscribers(); + Assert.AreEqual(1, first.Length); + + first[0] = null; + var second = evt.GetSubscribers(); + Assert.IsNotNull(second[0]); + } + } +} diff --git a/Tests/EventBusDispatchTests.cs.meta b/Tests/EventBusDispatchTests.cs.meta new file mode 100644 index 0000000..909457b --- /dev/null +++ b/Tests/EventBusDispatchTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11b7dddad0d215b47a7249e74a574105 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/EventBusPriorityTests.cs b/Tests/EventBusPriorityTests.cs new file mode 100644 index 0000000..262b7f1 --- /dev/null +++ b/Tests/EventBusPriorityTests.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using NUnit.Framework; + +namespace ShrinkEventBus.Tests +{ + [TestFixture] + public sealed class EventBusPriorityTests + { + private class BaseEvent : EventBase + { + } + + private sealed class DerivedEvent : BaseEvent + { + } + + [Test] + public void EnumPriority_ExecutesPhasesInOrder() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + bus.RegisterEvent(_ => order.Add("LOW"), EventPriority.LOW); + bus.RegisterEvent(_ => order.Add("MONITOR"), EventPriority.MONITOR); + bus.RegisterEvent(_ => order.Add("HIGHEST"), EventPriority.HIGHEST); + bus.RegisterEvent(_ => order.Add("NORMAL"), EventPriority.NORMAL); + + bus.TriggerEvent(new BaseEvent()); + + CollectionAssert.AreEqual(new[] { "HIGHEST", "NORMAL", "LOW", "MONITOR" }, order); + } + + [Test] + public void NumericPriority_HigherNumberRunsFirstWithinPhase() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + // 5 与 10 都映射到 NORMAL 档 + bus.RegisterEvent(_ => order.Add("p5"), 5); + bus.RegisterEvent(_ => order.Add("p10"), 10); + + bus.TriggerEvent(new BaseEvent()); + + CollectionAssert.AreEqual(new[] { "p10", "p5" }, order); + } + + [Test] + public void NumericPriority_TieKeepsRegistrationOrder() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + bus.RegisterEvent(_ => order.Add("first"), 0); + bus.RegisterEvent(_ => order.Add("second"), 0); + + bus.TriggerEvent(new BaseEvent()); + + CollectionAssert.AreEqual(new[] { "first", "second" }, order); + } + + [Test] + public void NumericPriorityZero_MapsToNormalPhase() + { + var bus = EventBus.CreateBus(); + bus.RegisterEvent(_ => { }, 0); + + var subscribers = bus.GetEventSubscribers(); + + Assert.AreEqual(1, subscribers.Length); + Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority); + } + + [Test] + public void RegisterEvent_WithoutPriority_ResolvesToNormal() + { + var bus = EventBus.CreateBus(); + + void Handler(BaseEvent evt) + { + } + + // 不带优先级的调用应唯一解析到枚举重载(NORMAL),不再有重载二义性 + bus.RegisterEvent(Handler); + + var subscribers = bus.GetEventSubscribers(); + Assert.AreEqual(1, subscribers.Length); + Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority); + } + + [Test] + public void ParentAndChildHandlers_MergeByNumericPriorityWithinPhase() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + bus.RegisterEvent(_ => order.Add("parent10"), 10); + bus.RegisterEvent(_ => order.Add("child5"), 5); + bus.RegisterEvent(_ => order.Add("child1"), 1); + + bus.TriggerEvent(new DerivedEvent()); + + CollectionAssert.AreEqual(new[] { "parent10", "child5", "child1" }, order); + } + + [Test] + public void ParentAndChildHandlers_TiePrefersChild() + { + var bus = EventBus.CreateBus(); + var order = new List(); + + bus.RegisterEvent(_ => order.Add("parent0"), 0); + bus.RegisterEvent(_ => order.Add("child0"), 0); + + bus.TriggerEvent(new DerivedEvent()); + + CollectionAssert.AreEqual(new[] { "child0", "parent0" }, order); + } + } +} diff --git a/Tests/EventBusPriorityTests.cs.meta b/Tests/EventBusPriorityTests.cs.meta new file mode 100644 index 0000000..8b254b3 --- /dev/null +++ b/Tests/EventBusPriorityTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc534f0e8598cab47b393ccffc90ad98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/EventBusRegistrationTests.cs b/Tests/EventBusRegistrationTests.cs new file mode 100644 index 0000000..1060261 --- /dev/null +++ b/Tests/EventBusRegistrationTests.cs @@ -0,0 +1,222 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace ShrinkEventBus.Tests +{ + [TestFixture] + public sealed class EventBusRegistrationTests + { + private class ProbeEvent : EventBase + { + } + + private sealed class PooledEvent : EventBase + { + public int Value { get; set; } + + protected override void OnReset() + { + Value = 0; + } + } + + private sealed class InstanceSubscriber + { + public int Hits; + + [EventSubscribe(EventPriority.NORMAL)] + private void OnProbe(ProbeEvent evt) + { + Hits++; + } + } + + private static class StaticSubscriber + { + public static int Hits; + + [EventSubscribe(EventPriority.NORMAL)] + private static void OnProbe(ProbeEvent evt) + { + Hits++; + } + } + + private sealed class InvalidSignatureSubscriber + { + [EventSubscribe(EventPriority.NORMAL)] + private void OnProbe(ProbeEvent evt, int extra) + { + } + } + + [EventBusSubscriber] + private sealed class EmptySubscriber + { + } + + private sealed class UnattributedSubscriber + { + [EventSubscribe(EventPriority.NORMAL)] + private void OnProbe(ProbeEvent evt) + { + } + } + + [Test] + public void Register_InstanceScan_RegistersAndUnregisters() + { + var bus = EventBus.CreateBus(); + var subscriber = new InstanceSubscriber(); + + bus.Register(subscriber); + Assert.IsTrue(bus.IsInstanceRegistered(subscriber)); + bus.TriggerEvent(new ProbeEvent()); + Assert.AreEqual(1, subscriber.Hits); + + bus.Unregister(subscriber); + Assert.IsFalse(bus.IsInstanceRegistered(subscriber)); + bus.TriggerEvent(new ProbeEvent()); + Assert.AreEqual(1, subscriber.Hits); + } + + [Test] + public void Register_StaticTypeScan_RegistersAndUnregisters() + { + var bus = EventBus.CreateBus(); + StaticSubscriber.Hits = 0; + + bus.Register(typeof(StaticSubscriber)); + bus.TriggerEvent(new ProbeEvent()); + Assert.AreEqual(1, StaticSubscriber.Hits); + + bus.Unregister(typeof(StaticSubscriber)); + bus.TriggerEvent(new ProbeEvent()); + Assert.AreEqual(1, StaticSubscriber.Hits); + } + + [Test] + public void Register_MethodInfo_RegistersSingleHandler() + { + var bus = EventBus.CreateBus(); + StaticSubscriber.Hits = 0; + var method = typeof(StaticSubscriber).GetMethod("OnProbe", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.IsNotNull(method); + + bus.Register(method); + bus.TriggerEvent(new ProbeEvent()); + Assert.AreEqual(1, StaticSubscriber.Hits); + + bus.Unregister(method); + bus.TriggerEvent(new ProbeEvent()); + Assert.AreEqual(1, StaticSubscriber.Hits); + } + + [Test] + public void Register_InvalidHandlerSignature_Throws() + { + var bus = EventBus.CreateBus(); + + Assert.Throws(() => bus.Register(new InvalidSignatureSubscriber())); + } + + [Test] + public void AutoRegister_TypeWithoutSubscriberAttribute_Throws() + { + var bus = EventBus.CreateBus(); + + Assert.Throws(() => bus.AutoRegister(new UnattributedSubscriber())); + } + + [Test] + public void AutoRegister_SubscriberWithoutHandlers_WarnsInsteadOfThrowing() + { + var bus = EventBus.CreateBus(); + + LogAssert.Expect(LogType.Warning, new Regex(@"has no \[EventSubscribe\] methods")); + Assert.DoesNotThrow(() => bus.AutoRegister(new EmptySubscriber())); + } + + [Test] + public void GetEventSubscribers_ReturnsDefensiveCopy() + { + var bus = EventBus.CreateBus(); + bus.RegisterEvent(_ => { }, EventPriority.NORMAL); + + var first = bus.GetEventSubscribers(); + Assert.AreEqual(1, first.Length); + + first[0] = null; + var second = bus.GetEventSubscribers(); + Assert.IsNotNull(second[0]); + } + + [Test] + public void UnregisterAllEventsForObject_RemovesScannedHandlers() + { + var bus = EventBus.CreateBus(); + var subscriber = new InstanceSubscriber(); + bus.Register(subscriber); + + bus.UnregisterAllEventsForObject(subscriber); + bus.TriggerEvent(new ProbeEvent()); + + Assert.AreEqual(0, subscriber.Hits); + } + + [Test] + public void EventPool_ReleaseResetsAndReusesInstance() + { + var evt = EventPool.Get(); + evt.Value = 42; + + EventPool.Release(evt); + var reused = EventPool.Get(); + + Assert.AreSame(evt, reused); + Assert.AreEqual(0, reused.Value); + + EventPool.Release(reused); + } + + [Test] + public void EventPool_DoubleRelease_DoesNotDuplicatePoolEntry() + { + var evt = EventPool.Get(); + + EventPool.Release(evt); + EventPool.Release(evt); + + var first = EventPool.Get(); + var second = EventPool.Get(); + + Assert.AreSame(evt, first); + Assert.AreNotSame(evt, second); + + EventPool.Release(first); + EventPool.Release(second); + } + + [Test] + public void EventPool_DisposeReturnsToPool() + { + PooledEvent captured; + using (var evt = EventPool.Get()) + { + captured = evt; + evt.Value = 7; + } + + var reused = EventPool.Get(); + Assert.AreSame(captured, reused); + Assert.AreEqual(0, reused.Value); + + EventPool.Release(reused); + } + } +} diff --git a/Tests/EventBusRegistrationTests.cs.meta b/Tests/EventBusRegistrationTests.cs.meta new file mode 100644 index 0000000..cbec877 --- /dev/null +++ b/Tests/EventBusRegistrationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4570cac2813143c40b9a8edeadf19057 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/EventBusSubscriptionTests.cs b/Tests/EventBusSubscriptionTests.cs new file mode 100644 index 0000000..69d8266 --- /dev/null +++ b/Tests/EventBusSubscriptionTests.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using UnityEngine; + +namespace ShrinkEventBus.Tests +{ + [TestFixture] + public sealed class EventBusSubscriptionTests + { + private sealed class SampleEvent : EventBase + { + public int Value { get; set; } + } + + [SetUp] + public void SetUp() + { + EventBus.UnregisterAllEvents(); + } + + [TearDown] + public void TearDown() + { + EventBus.UnregisterAllEvents(); + } + + [Test] + public void SubscribeEvent_Dispose_RemovesOnlyOwnedSubscription() + { + var firstHits = 0; + var secondHits = 0; + + using var first = EventBus.SubscribeEvent(_ => firstHits++); + using var second = EventBus.SubscribeEvent(_ => secondHits++); + + EventBus.TriggerEvent(new SampleEvent()); + Assert.AreEqual(1, firstHits); + Assert.AreEqual(1, secondHits); + + first.Dispose(); + + EventBus.TriggerEvent(new SampleEvent()); + Assert.AreEqual(1, firstHits); + Assert.AreEqual(2, secondHits); + } + + [Test] + public void ActiveSubscriptionSnapshot_ContainsExpectedMetadata() + { + using var subscription = EventBus.SubscribeEvent(_ => { }, EventPriority.HIGH, true); + + var snapshots = EventBus.GetActiveSubscriptionsSnapshot(); + + Assert.AreEqual(1, snapshots.Count); + Assert.AreEqual(subscription.SubscriptionId, snapshots[0].SubscriptionId); + Assert.AreEqual(typeof(SampleEvent), snapshots[0].EventType); + Assert.AreEqual(EventPriority.HIGH, snapshots[0].Priority); + Assert.IsTrue(snapshots[0].ReceiveCanceled); + Assert.IsFalse(string.IsNullOrWhiteSpace(snapshots[0].MethodName)); + } + + [Test] + public void RegisterEvent_StillWorksWithoutDisposableSubscription() + { + var hits = 0; + + void Handler(SampleEvent evt) + { + hits += evt.Value; + } + + EventBus.RegisterEvent(Handler); + EventBus.TriggerEvent(new SampleEvent { Value = 3 }); + + Assert.AreEqual(3, hits); + + EventBus.UnregisterEvent(Handler); + EventBus.TriggerEvent(new SampleEvent { Value = 5 }); + + Assert.AreEqual(3, hits); + } + + [Test] + public void ActiveSubscriptionSnapshot_EmptyAfterDispose() + { + var subscription = EventBus.SubscribeEvent(_ => Debug.Log("noop")); + subscription.Dispose(); + + var snapshots = EventBus.GetActiveSubscriptionsSnapshot(); + + Assert.AreEqual(0, snapshots.Count); + Assert.IsTrue(subscription.IsDisposed); + } + } +} diff --git a/Tests/EventBusSubscriptionTests.cs.meta b/Tests/EventBusSubscriptionTests.cs.meta new file mode 100644 index 0000000..c1e8486 --- /dev/null +++ b/Tests/EventBusSubscriptionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5ea531619ceaeb46af4f56454624ef7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/ShrinkEventBus.Tests.asmdef b/Tests/ShrinkEventBus.Tests.asmdef new file mode 100644 index 0000000..69004b2 --- /dev/null +++ b/Tests/ShrinkEventBus.Tests.asmdef @@ -0,0 +1,23 @@ +{ + "name": "ShrinkEventBus.Tests", + "rootNamespace": "ShrinkEventBus.Tests", + "references": [ + "ShrinkEventBus.Runtime", + "UniTask", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Tests/ShrinkEventBus.Tests.asmdef.meta b/Tests/ShrinkEventBus.Tests.asmdef.meta new file mode 100644 index 0000000..c6c5e74 --- /dev/null +++ b/Tests/ShrinkEventBus.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8c47d36b8919bbf4d9c39817eaf577a9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 5328b23..7d6ff53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-eventbus", - "version": "1.1.5", + "version": "1.3.0", "displayName": "ShrinkEventBus", "description": "高性能、类型安全的 Unity 事件总线,支持优先级调度与自动注册。", "unity": "2022.3",