feat: 重构事件总线为可实例化架构并发布 1.3.0
- 新增 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。
This commit is contained in:
41 files changed
+3168
-888
No files matched your search
+64
-49
@@ -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 ║
|
║ 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 × 无订阅者(基线)
|
▶ TriggerEvent × 无订阅者(基线)
|
||||||
总耗时 : 68.481 ms
|
总耗时 : 111.466 ms
|
||||||
单次 : 0.0685 μs/op
|
单次 : 0.1115 μs/op
|
||||||
吞吐量 : 14602548 ops/s
|
吞吐量 : 8971378 ops/s
|
||||||
|
|
||||||
▶ TriggerEvent × 单订阅者同步
|
▶ TriggerEvent × 单订阅者同步
|
||||||
总耗时 : 115.262 ms
|
总耗时 : 163.073 ms
|
||||||
单次 : 0.1153 μs/op
|
单次 : 0.1631 μs/op
|
||||||
吞吐量 : 8675871 ops/s
|
吞吐量 : 6132242 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent × 父事件监听子事件
|
||||||
|
总耗时 : 171.424 ms
|
||||||
|
单次 : 0.1714 μs/op
|
||||||
|
吞吐量 : 5833479 ops/s
|
||||||
|
|
||||||
▶ TriggerEvent × 30 订阅者同步
|
▶ TriggerEvent × 30 订阅者同步
|
||||||
总耗时 : 7.376 ms
|
总耗时 : 38.639 ms
|
||||||
单次 : 0.7376 μs/op
|
单次 : 3.8639 μs/op
|
||||||
吞吐量 : 1355712 ops/s
|
吞吐量 : 258805 ops/s
|
||||||
|
|
||||||
|
▶ TriggerEvent(Phase=HIGH) × 3 phase 监听
|
||||||
|
总耗时 : 164.197 ms
|
||||||
|
单次 : 0.1642 μs/op
|
||||||
|
吞吐量 : 6090238 ops/s
|
||||||
|
|
||||||
▶ TriggerEventAsync × 单订阅者异步
|
▶ TriggerEventAsync × 单订阅者异步
|
||||||
总耗时 : 0.684 ms
|
总耗时 : 0.856 ms
|
||||||
单次 : 0.6840 μs/op
|
单次 : 0.8557 μs/op
|
||||||
吞吐量 : 1461988 ops/s
|
吞吐量 : 1168634 ops/s
|
||||||
|
|
||||||
▶ RegisterEvent
|
▶ RegisterEvent(delegate)
|
||||||
总耗时 : 18.780 ms
|
总耗时 : 14.418 ms
|
||||||
单次 : 3.7561 μs/op
|
单次 : 2.8836 μs/op
|
||||||
吞吐量 : 266236 ops/s
|
吞吐量 : 346791 ops/s
|
||||||
|
|
||||||
▶ UnregisterEvent
|
▶ UnregisterEvent(delegate)
|
||||||
总耗时 : 117.646 ms
|
总耗时 : 3.427 ms
|
||||||
单次 : 23.5292 μs/op
|
单次 : 0.6853 μs/op
|
||||||
吞吐量 : 42500 ops/s
|
吞吐量 : 1459130 ops/s
|
||||||
|
|
||||||
▶ TriggerEvent × new() [GC Gen0=6457]
|
▶ Register/Unregister(instance scan)
|
||||||
总耗时 : 7684.313 ms
|
总耗时 : 13.097 ms
|
||||||
单次 : 7.6843 μs/op
|
单次 : 13.0971 μs/op
|
||||||
吞吐量 : 130135 ops/s
|
吞吐量 : 76353 ops/s
|
||||||
|
|
||||||
▶ TriggerEvent × Pool.Get() [GC Gen0= 91]
|
▶ Register/Unregister(static type scan)
|
||||||
总耗时 : 348.948 ms
|
总耗时 : 9.337 ms
|
||||||
单次 : 0.3489 μs/op
|
单次 : 9.3374 μs/op
|
||||||
吞吐量 : 2865761 ops/s
|
吞吐量 : 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 订阅者)
|
▶ TriggerEvent × 已取消事件跳过(10 订阅者)
|
||||||
总耗时 : 25.282 ms
|
总耗时 : 52.777 ms
|
||||||
单次 : 0.2528 μs/op
|
单次 : 0.5278 μs/op
|
||||||
吞吐量 : 3955352 ops/s
|
吞吐量 : 1894776 ops/s
|
||||||
|
|
||||||
══════════════════════════════════════════════════
|
══════════════════════════════════════════════════
|
||||||
全部测试完成
|
全部测试完成
|
||||||
|
|
||||||
|
对比 1.1.x 旧样本:new() 路径 7684ms / GC Gen0=6457 → 266ms / GC Gen0=1
|
||||||
|
(EventBase 不再每实例分配内置 ListenerList,new() 路径 GC 基本归零,
|
||||||
|
对象池从“高频必需”降级为可选优化。)
|
||||||
@@ -2,6 +2,49 @@
|
|||||||
|
|
||||||
本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。
|
本文件记录 `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<T>`(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
|
## [1.1.5] - 2026-04-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,78 +1,198 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Mono.Cecil;
|
using Mono.Cecil;
|
||||||
using Mono.Cecil.Cil;
|
using Mono.Cecil.Cil;
|
||||||
using Mono.Cecil.Rocks;
|
using Mono.Cecil.Rocks;
|
||||||
|
using Mono.Cecil.Pdb;
|
||||||
using Unity.CompilationPipeline.Common.Diagnostics;
|
using Unity.CompilationPipeline.Common.Diagnostics;
|
||||||
using Unity.CompilationPipeline.Common.ILPostProcessing;
|
using Unity.CompilationPipeline.Common.ILPostProcessing;
|
||||||
|
|
||||||
namespace ShrinkEventBus.CodeGen
|
namespace ShrinkEventBus.CodeGen
|
||||||
{
|
{
|
||||||
public class EventBusILPostProcessor : ILPostProcessor
|
public sealed class EventBusILPostProcessor : ILPostProcessor
|
||||||
{
|
{
|
||||||
private readonly List<DiagnosticMessage> _diagnostics = new();
|
private const string RuntimeAssemblyName = "ShrinkEventBus.Runtime";
|
||||||
private PostProcessorAssemblyResolver _assemblyResolver;
|
private static readonly string[] SharedCoverageAssemblyNames =
|
||||||
private MethodReference _autoRegisterMethodRef;
|
{
|
||||||
private MethodReference _unregisterInstanceMethodRef;
|
"ShrinkCommand.Runtime",
|
||||||
|
"ShrinkNetwork.Runtime",
|
||||||
|
"ShrinkApp.Core.Runtime"
|
||||||
|
};
|
||||||
|
|
||||||
public override ILPostProcessor GetInstance() => this;
|
public override ILPostProcessor GetInstance() => this;
|
||||||
|
|
||||||
public override bool WillProcess(ICompiledAssembly compiledAssembly)
|
public override bool WillProcess(ICompiledAssembly compiledAssembly)
|
||||||
{
|
{
|
||||||
return compiledAssembly.References.Any(
|
if (!ReferencesAssembly(compiledAssembly, RuntimeAssemblyName))
|
||||||
r => Path.GetFileNameWithoutExtension(r) == "ShrinkEventBus.Runtime");
|
return false;
|
||||||
|
|
||||||
|
if (ShouldDeferToSharedCodeGen(compiledAssembly))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
|
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
|
||||||
{
|
{
|
||||||
if (!WillProcess(compiledAssembly)) return null;
|
var diagnostics = new List<DiagnosticMessage>();
|
||||||
_diagnostics.Clear();
|
if (!WillProcess(compiledAssembly))
|
||||||
|
return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics);
|
||||||
|
|
||||||
var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
|
var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
|
||||||
var mainModule = assemblyDefinition.MainModule;
|
var module = assemblyDefinition.MainModule;
|
||||||
|
|
||||||
bool needWeaving = false;
|
try
|
||||||
foreach (var type in mainModule.Types)
|
|
||||||
{
|
{
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!needWeaving) return null;
|
return false;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
if (awake == null)
|
||||||
{
|
{
|
||||||
var baseAwakeRef = FindBaseMethodReference(type, "Awake", module);
|
var baseAwakeRef = FindBaseMethodReference(type, "Awake", module);
|
||||||
@@ -85,7 +205,7 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
il.Emit(OpCodes.Ldarg_0);
|
il.Emit(OpCodes.Ldarg_0);
|
||||||
il.Emit(OpCodes.Call, baseAwakeRef);
|
il.Emit(OpCodes.Call, baseAwakeRef);
|
||||||
il.Emit(OpCodes.Ldarg_0);
|
il.Emit(OpCodes.Ldarg_0);
|
||||||
il.Emit(OpCodes.Call, _autoRegisterMethodRef);
|
il.Emit(OpCodes.Call, autoRegisterMethodRef);
|
||||||
il.Emit(OpCodes.Ret);
|
il.Emit(OpCodes.Ret);
|
||||||
type.Methods.Add(awake);
|
type.Methods.Add(awake);
|
||||||
return;
|
return;
|
||||||
@@ -103,16 +223,16 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
var instructions = new List<Instruction>
|
var instructions = new List<Instruction>
|
||||||
{
|
{
|
||||||
processor.Create(OpCodes.Ldarg_0),
|
processor.Create(OpCodes.Ldarg_0),
|
||||||
processor.Create(OpCodes.Call, _autoRegisterMethodRef),
|
processor.Create(OpCodes.Call, autoRegisterMethodRef),
|
||||||
processor.Create(OpCodes.Nop)
|
processor.Create(OpCodes.Nop)
|
||||||
};
|
};
|
||||||
instructions.Reverse();
|
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)
|
if (onDestroy == null)
|
||||||
{
|
{
|
||||||
var baseOnDestroyRef = FindBaseMethodReference(type, "OnDestroy", module);
|
var baseOnDestroyRef = FindBaseMethodReference(type, "OnDestroy", module);
|
||||||
@@ -123,7 +243,7 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
module.TypeSystem.Void);
|
module.TypeSystem.Void);
|
||||||
var il = onDestroy.Body.GetILProcessor();
|
var il = onDestroy.Body.GetILProcessor();
|
||||||
il.Emit(OpCodes.Ldarg_0);
|
il.Emit(OpCodes.Ldarg_0);
|
||||||
il.Emit(OpCodes.Call, _unregisterInstanceMethodRef);
|
il.Emit(OpCodes.Call, unregisterMethodRef);
|
||||||
il.Emit(OpCodes.Ldarg_0);
|
il.Emit(OpCodes.Ldarg_0);
|
||||||
il.Emit(OpCodes.Call, baseOnDestroyRef);
|
il.Emit(OpCodes.Call, baseOnDestroyRef);
|
||||||
il.Emit(OpCodes.Ret);
|
il.Emit(OpCodes.Ret);
|
||||||
@@ -143,14 +263,14 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
var instructions = new List<Instruction>
|
var instructions = new List<Instruction>
|
||||||
{
|
{
|
||||||
processor.Create(OpCodes.Ldarg_0),
|
processor.Create(OpCodes.Ldarg_0),
|
||||||
processor.Create(OpCodes.Call, _unregisterInstanceMethodRef),
|
processor.Create(OpCodes.Call, unregisterMethodRef),
|
||||||
processor.Create(OpCodes.Nop)
|
processor.Create(OpCodes.Nop)
|
||||||
};
|
};
|
||||||
instructions.Reverse();
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -158,11 +278,11 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
while (baseTypeRef != null)
|
while (baseTypeRef != null)
|
||||||
{
|
{
|
||||||
var baseTypeDef = baseTypeRef.Resolve();
|
var baseTypeDef = baseTypeRef.Resolve();
|
||||||
if (baseTypeDef == null) break;
|
if (baseTypeDef == null)
|
||||||
|
break;
|
||||||
var method = baseTypeDef.Methods.FirstOrDefault(
|
|
||||||
m => m.Name == methodName && !m.IsStatic && m.IsVirtual);
|
|
||||||
|
|
||||||
|
var method = baseTypeDef.Methods.FirstOrDefault(candidate =>
|
||||||
|
candidate.Name == methodName && !candidate.IsStatic && candidate.IsVirtual);
|
||||||
if (method != null)
|
if (method != null)
|
||||||
{
|
{
|
||||||
if (baseTypeRef is GenericInstanceType genericInstance)
|
if (baseTypeRef is GenericInstanceType genericInstance)
|
||||||
@@ -187,106 +307,69 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignored
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportReferences(ModuleDefinition module)
|
private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName)
|
||||||
{
|
{
|
||||||
var runtimeModule = FindRuntimeModule(module);
|
var resolved = Type.GetType($"{fullName}, {assemblyName}", false);
|
||||||
if (runtimeModule == null)
|
return resolved == null ? null : module.ImportReference(resolved);
|
||||||
{
|
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ModuleDefinition FindRuntimeModule(ModuleDefinition module)
|
private static MethodReference? FindTypeArrayConstructor(ModuleDefinition module, string fullName, string assemblyName)
|
||||||
{
|
{
|
||||||
ModuleDefinition result = null;
|
var typeRef = FindType(module, fullName, assemblyName);
|
||||||
var visited = new HashSet<string>();
|
var typeDef = typeRef?.Resolve();
|
||||||
SearchRecursive(_assemblyResolver.Resolve(module.Assembly.Name), ref result, visited);
|
var ctor = typeDef?.Methods.FirstOrDefault(method =>
|
||||||
return result;
|
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<string> visited)
|
private static void AddAssemblyTypeArrayAttribute(ModuleDefinition module, MethodReference ctor, TypeReference[] types)
|
||||||
{
|
{
|
||||||
if (asm == null) return;
|
var attribute = new CustomAttribute(ctor);
|
||||||
|
var typeTypeRef = module.ImportReference(typeof(Type));
|
||||||
foreach (var mod in asm.Modules)
|
attribute.ConstructorArguments.Add(new CustomAttributeArgument(
|
||||||
{
|
module.ImportReference(typeof(Type[])),
|
||||||
if (mod.Name == "ShrinkEventBus.Runtime.dll" || mod.Name == "ShrinkEventBus.Runtime")
|
types.Select(type => new CustomAttributeArgument(typeTypeRef, type)).ToArray()));
|
||||||
{
|
module.Assembly.CustomAttributes.Add(attribute);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
|
private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
|
||||||
{
|
{
|
||||||
_assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly);
|
var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly);
|
||||||
var readerParameters = new ReaderParameters
|
var readerParameters = new ReaderParameters
|
||||||
{
|
{
|
||||||
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()),
|
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData.ToArray()),
|
||||||
SymbolReaderProvider = new PortablePdbReaderProvider(),
|
SymbolReaderProvider = new PdbReaderProvider(),
|
||||||
AssemblyResolver = _assemblyResolver,
|
AssemblyResolver = assemblyResolver,
|
||||||
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
|
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
|
||||||
ReadingMode = ReadingMode.Immediate
|
ReadingMode = ReadingMode.Immediate
|
||||||
};
|
};
|
||||||
var peStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray());
|
|
||||||
var assemblyDef = AssemblyDefinition.ReadAssembly(peStream, readerParameters);
|
var assemblyDefinition = AssemblyDefinition.ReadAssembly(
|
||||||
_assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDef);
|
new MemoryStream(compiledAssembly.InMemoryAssembly.PeData.ToArray()),
|
||||||
return assemblyDef;
|
readerParameters);
|
||||||
|
assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition);
|
||||||
|
return assemblyDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ILPostProcessResult GetResult(AssemblyDefinition asm, List<DiagnosticMessage> diagnostics)
|
private static ILPostProcessResult GetResult(AssemblyDefinition assemblyDefinition, List<DiagnosticMessage> diagnostics)
|
||||||
{
|
{
|
||||||
var pe = new MemoryStream();
|
var pe = new MemoryStream();
|
||||||
var pdb = new MemoryStream();
|
var pdb = new MemoryStream();
|
||||||
asm.Write(pe, new WriterParameters
|
assemblyDefinition.Write(pe, new WriterParameters
|
||||||
{
|
{
|
||||||
SymbolWriterProvider = new PortablePdbWriterProvider(),
|
SymbolWriterProvider = new PdbWriterProvider(),
|
||||||
SymbolStream = pdb,
|
SymbolStream = pdb,
|
||||||
WriteSymbols = true
|
WriteSymbols = true
|
||||||
});
|
});
|
||||||
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Mono.Cecil;
|
using Mono.Cecil;
|
||||||
@@ -6,11 +8,11 @@ using Unity.CompilationPipeline.Common.ILPostProcessing;
|
|||||||
|
|
||||||
namespace ShrinkEventBus.CodeGen
|
namespace ShrinkEventBus.CodeGen
|
||||||
{
|
{
|
||||||
internal class PostProcessorAssemblyResolver : IAssemblyResolver
|
internal sealed class PostProcessorAssemblyResolver : IAssemblyResolver
|
||||||
{
|
{
|
||||||
private readonly string[] _references;
|
private readonly string[] _references;
|
||||||
private readonly Dictionary<string, AssemblyDefinition> _cache = new();
|
private readonly Dictionary<string, AssemblyDefinition> _cache = new();
|
||||||
private AssemblyDefinition _self;
|
private AssemblyDefinition? _self;
|
||||||
|
|
||||||
public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
|
public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
|
||||||
{
|
{
|
||||||
@@ -22,10 +24,12 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
_self = assemblyDefinition;
|
_self = assemblyDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AssemblyDefinition Resolve(AssemblyNameReference name)
|
public AssemblyDefinition? Resolve(AssemblyNameReference name)
|
||||||
=> Resolve(name, new ReaderParameters(ReadingMode.Deferred));
|
{
|
||||||
|
return Resolve(name, new ReaderParameters(ReadingMode.Deferred));
|
||||||
|
}
|
||||||
|
|
||||||
public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
|
public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters)
|
||||||
{
|
{
|
||||||
lock (_cache)
|
lock (_cache)
|
||||||
{
|
{
|
||||||
@@ -33,10 +37,12 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
return _self;
|
return _self;
|
||||||
|
|
||||||
var path = FindPath(name);
|
var path = FindPath(name);
|
||||||
if (path == null) return null;
|
if (path == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
var key = $"{path}{File.GetLastWriteTime(path)}";
|
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;
|
parameters.AssemblyResolver = this;
|
||||||
var ms = ReadFileWithRetry(path);
|
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)
|
if (Path.GetFileNameWithoutExtension(reference) == name.Name)
|
||||||
return r;
|
return reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dirs = new HashSet<string>();
|
var dirs = new HashSet<string>();
|
||||||
foreach (var r in _references)
|
foreach (var reference in _references)
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(r);
|
var dir = Path.GetDirectoryName(reference);
|
||||||
if (dir != null) dirs.Add(dir);
|
if (dir != null)
|
||||||
|
dirs.Add(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var dir in dirs)
|
foreach (var dir in dirs)
|
||||||
{
|
{
|
||||||
var candidate = Path.Combine(dir, $"{name.Name}.dll");
|
var candidate = Path.Combine(dir, $"{name.Name}.dll");
|
||||||
if (File.Exists(candidate)) return candidate;
|
if (File.Exists(candidate))
|
||||||
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -81,17 +89,19 @@ namespace ShrinkEventBus.CodeGen
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bytes = File.ReadAllBytes(path);
|
return new MemoryStream(File.ReadAllBytes(path));
|
||||||
return new MemoryStream(bytes);
|
|
||||||
}
|
}
|
||||||
catch (IOException) when (i < retries - 1)
|
catch (IOException) when (i < retries - 1)
|
||||||
{
|
{
|
||||||
Thread.Sleep(100);
|
Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new IOException($"[ShrinkEventBus] 无法读取文件: {path}");
|
|
||||||
|
throw new IOException($"[ShrinkEventBus.CodeGen] 无法读取文件: {path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() { }
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: aeea50f82bab4ee2833c671d801c95e3
|
guid: aeea50f82bab4ee2833c671d801c95e3
|
||||||
timeCreated: 1772908894
|
timeCreated: 1772908894
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
using System.Linq;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Mono.Cecil;
|
using Mono.Cecil;
|
||||||
|
|
||||||
namespace ShrinkEventBus.CodeGen
|
namespace ShrinkEventBus.CodeGen
|
||||||
{
|
{
|
||||||
internal class PostProcessorReflectionImporter : DefaultReflectionImporter
|
internal sealed class PostProcessorReflectionImporter : DefaultReflectionImporter
|
||||||
{
|
{
|
||||||
private const string CoreLibName = "System.Private.CoreLib";
|
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(
|
_corlib = module.AssemblyReferences.FirstOrDefault(
|
||||||
a => a.Name is "mscorlib" or "netstandard");
|
assembly => assembly.Name is "mscorlib" or "netstandard");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override AssemblyNameReference ImportReference(AssemblyName reference)
|
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)
|
public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
|
||||||
=> new PostProcessorReflectionImporter(module);
|
{
|
||||||
|
return new PostProcessorReflectionImporter(module);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 0a6c2401798b42f0bbcde72796d2fa9c
|
guid: 0a6c2401798b42f0bbcde72796d2fa9c
|
||||||
timeCreated: 1772908906
|
timeCreated: 1772908906
|
||||||
@@ -20,4 +20,4 @@
|
|||||||
"defineConstraints": [],
|
"defineConstraints": [],
|
||||||
"versionDefines": [],
|
"versionDefines": [],
|
||||||
"noEngineReferences": false
|
"noEngineReferences": false
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,10 @@ namespace ShrinkEventBus.Editor
|
|||||||
private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All;
|
private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All;
|
||||||
private bool _collapseSameEventType = true;
|
private bool _collapseSameEventType = true;
|
||||||
private const int MaxLogCount = 100;
|
private const int MaxLogCount = 100;
|
||||||
|
private const double LiveTrackerRefreshIntervalSeconds = 0.1d;
|
||||||
private readonly List<EventLogRecord> _logRecords = new();
|
private readonly List<EventLogRecord> _logRecords = new();
|
||||||
|
private bool _isLiveTrackerDirty;
|
||||||
|
private double _nextLiveTrackerRefreshTime;
|
||||||
|
|
||||||
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
|
[MenuItem("ShrinkSDK/事件总线/事件查看器")]
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
@@ -74,8 +77,17 @@ namespace ShrinkEventBus.Editor
|
|||||||
window.Show();
|
window.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable() => EventBus.OnEventTriggeredForEditor += RecordEventLog;
|
private void OnEnable()
|
||||||
private void OnDisable() => EventBus.OnEventTriggeredForEditor -= RecordEventLog;
|
{
|
||||||
|
EventBus.OnEventTriggeredForEditor += RecordEventLog;
|
||||||
|
EditorApplication.update += OnEditorUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
EventBus.OnEventTriggeredForEditor -= RecordEventLog;
|
||||||
|
EditorApplication.update -= OnEditorUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateGUI()
|
private void CreateGUI()
|
||||||
{
|
{
|
||||||
@@ -166,7 +178,7 @@ namespace ShrinkEventBus.Editor
|
|||||||
searchField.RegisterValueChangedCallback(evt =>
|
searchField.RegisterValueChangedCallback(evt =>
|
||||||
{
|
{
|
||||||
_liveLogSearchString = evt.newValue ?? string.Empty;
|
_liveLogSearchString = evt.newValue ?? string.Empty;
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
});
|
});
|
||||||
toolbar.Add(searchField);
|
toolbar.Add(searchField);
|
||||||
|
|
||||||
@@ -175,19 +187,19 @@ namespace ShrinkEventBus.Editor
|
|||||||
{
|
{
|
||||||
_liveLogFilterMode = LiveLogFilterMode.All;
|
_liveLogFilterMode = LiveLogFilterMode.All;
|
||||||
filterMenu.text = "全部事件";
|
filterMenu.text = "全部事件";
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
});
|
});
|
||||||
filterMenu.menu.AppendAction("仅有监听者", _ =>
|
filterMenu.menu.AppendAction("仅有监听者", _ =>
|
||||||
{
|
{
|
||||||
_liveLogFilterMode = LiveLogFilterMode.HasListeners;
|
_liveLogFilterMode = LiveLogFilterMode.HasListeners;
|
||||||
filterMenu.text = "仅有监听者";
|
filterMenu.text = "仅有监听者";
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
});
|
});
|
||||||
filterMenu.menu.AppendAction("仅无监听者", _ =>
|
filterMenu.menu.AppendAction("仅无监听者", _ =>
|
||||||
{
|
{
|
||||||
_liveLogFilterMode = LiveLogFilterMode.NoListeners;
|
_liveLogFilterMode = LiveLogFilterMode.NoListeners;
|
||||||
filterMenu.text = "仅无监听者";
|
filterMenu.text = "仅无监听者";
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
});
|
});
|
||||||
toolbar.Add(filterMenu);
|
toolbar.Add(filterMenu);
|
||||||
|
|
||||||
@@ -199,7 +211,7 @@ namespace ShrinkEventBus.Editor
|
|||||||
collapseToggle.RegisterValueChangedCallback(evt =>
|
collapseToggle.RegisterValueChangedCallback(evt =>
|
||||||
{
|
{
|
||||||
_collapseSameEventType = evt.newValue;
|
_collapseSameEventType = evt.newValue;
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
});
|
});
|
||||||
toolbar.Add(collapseToggle);
|
toolbar.Add(collapseToggle);
|
||||||
toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" });
|
toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" });
|
||||||
@@ -220,7 +232,7 @@ namespace ShrinkEventBus.Editor
|
|||||||
|
|
||||||
_liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } };
|
_liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } };
|
||||||
container.Add(_liveTrackerScrollView);
|
container.Add(_liveTrackerScrollView);
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +240,31 @@ namespace ShrinkEventBus.Editor
|
|||||||
{
|
{
|
||||||
_subscribersTab.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None;
|
_subscribersTab.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
_liveTrackerTab.style.display = index == 1 ? 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()
|
private void RefreshSubscribersView()
|
||||||
@@ -240,20 +276,14 @@ namespace ShrinkEventBus.Editor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var field = typeof(EventBus).GetField("EventHandlers", BindingFlags.NonPublic | BindingFlags.Static);
|
var snapshots = EventBus.GetAllSubscribersSnapshot();
|
||||||
if (field == null) return;
|
foreach (var entry in snapshots)
|
||||||
var dict = field.GetValue(null) as IDictionary;
|
|
||||||
if (dict == null) return;
|
|
||||||
|
|
||||||
foreach (DictionaryEntry entry in dict)
|
|
||||||
{
|
{
|
||||||
var eventType = entry.Key as Type;
|
var eventType = entry.Key;
|
||||||
if (!string.IsNullOrEmpty(_searchString) &&
|
if (!string.IsNullOrEmpty(_searchString) &&
|
||||||
!eventType.Name.ToLower().Contains(_searchString.ToLower())) continue;
|
!eventType.Name.ToLower().Contains(_searchString.ToLower())) continue;
|
||||||
|
|
||||||
var listenerListObj = entry.Value;
|
var handlers = entry.Value;
|
||||||
var handlers =
|
|
||||||
listenerListObj.GetType().GetMethod("GetHandlers")?.Invoke(listenerListObj, null) as Array;
|
|
||||||
|
|
||||||
var foldout = new Foldout
|
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;
|
if (!EventBus.EnableDebugRecord) return;
|
||||||
_logRecords.Insert(0, CreateEventLogRecord(evt, eventName, senderInfo));
|
_logRecords.Add(CreateEventLogRecord(evt, eventName, senderInfo, handlers));
|
||||||
if (_logRecords.Count > MaxLogCount)
|
if (_logRecords.Count > MaxLogCount)
|
||||||
_logRecords.RemoveAt(_logRecords.Count - 1);
|
_logRecords.RemoveAt(0);
|
||||||
RefreshLiveTrackerView();
|
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 timestamp = DateTime.Now;
|
||||||
var list = evt.GetListenerList();
|
|
||||||
var handlers = list?.GetHandlers() ?? Array.Empty<EventHandlerInfo>();
|
|
||||||
var hasListeners = handlers.Length > 0;
|
var hasListeners = handlers.Length > 0;
|
||||||
var paramsList = DumpEventParams(evt);
|
var paramsList = DumpEventParams(evt);
|
||||||
var handlerRows = new List<HandlerDisplayData>(handlers.Length);
|
var handlerRows = new List<HandlerDisplayData>(handlers.Length);
|
||||||
@@ -325,12 +354,15 @@ namespace ShrinkEventBus.Editor
|
|||||||
private void RefreshLiveTrackerView()
|
private void RefreshLiveTrackerView()
|
||||||
{
|
{
|
||||||
if (_liveTrackerScrollView == null) return;
|
if (_liveTrackerScrollView == null) return;
|
||||||
|
_isLiveTrackerDirty = false;
|
||||||
|
_nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup + LiveTrackerRefreshIntervalSeconds;
|
||||||
|
|
||||||
_liveTrackerScrollView.Clear();
|
_liveTrackerScrollView.Clear();
|
||||||
|
|
||||||
var filteredRecords = new List<EventLogRecord>();
|
var filteredRecords = new List<EventLogRecord>(_logRecords.Count);
|
||||||
foreach (var record in _logRecords)
|
for (var i = _logRecords.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
|
var record = _logRecords[i];
|
||||||
if (MatchesLiveLogFilter(record))
|
if (MatchesLiveLogFilter(record))
|
||||||
filteredRecords.Add(record);
|
filteredRecords.Add(record);
|
||||||
}
|
}
|
||||||
@@ -425,15 +457,36 @@ namespace ShrinkEventBus.Editor
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
foldout.Add(summary);
|
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)
|
foreach (var record in group.Records)
|
||||||
{
|
{
|
||||||
var row = CreateLogEntryElement(record);
|
var row = CreateLogEntryElement(record);
|
||||||
row.style.marginLeft = 12;
|
row.style.marginLeft = 12;
|
||||||
foldout.Add(row);
|
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)
|
private VisualElement CreateLogEntryElement(EventLogRecord record)
|
||||||
@@ -714,7 +767,7 @@ namespace ShrinkEventBus.Editor
|
|||||||
private void ClearLogs()
|
private void ClearLogs()
|
||||||
{
|
{
|
||||||
_logRecords.Clear();
|
_logRecords.Clear();
|
||||||
RefreshLiveTrackerView();
|
RequestLiveTrackerRefresh(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenScriptByClassName(string className)
|
private void OpenScriptByClassName(string className)
|
||||||
|
|||||||
@@ -7,18 +7,34 @@
|
|||||||
| 特性 | 说明 |
|
| 特性 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 |
|
| 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 |
|
||||||
| ⚡ **高性能热路径** | 泛型静态缓存 `EventCache<T>` 绕过字典查找,触发路径几乎零开销 |
|
| ⚡ **高性能热路径** | 注册期预编译 invoker(无反射调用、无装箱),派发走缓存快照数组,监听者快照零拷贝挂接 |
|
||||||
| 🤖 **零侵入自动注册** | 标记 `[EventBusSubscriber]` 即可,ILPostProcessor 编译期自动织入注册与反注册逻辑,动态创建的对象也无需手写任何代码 |
|
| 🤖 **零侵入自动注册** | 标记 `[EventBusSubscriber]` 即可,ILPostProcessor 编译期自动织入注册与反注册逻辑,动态创建的对象也无需手写任何代码 |
|
||||||
|
| 🧾 **静态订阅清单** | 静态 `[EventBusSubscriber]` 现可通过编译期注册表收口,避免默认总线启动时全域扫描所有类型 |
|
||||||
| 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 |
|
| 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 |
|
||||||
| 🔄 **同步 & 异步** | 统一支持 `Action`、`UniTask`、`Task` 三种 handler 形式 |
|
| 🔄 **同步 & 异步** | 统一支持 `Action`(同步)与 `UniTask`(异步)两种 handler 形式 |
|
||||||
| 🧵 **线程安全** | 注册/注销操作全程加锁保护 |
|
| 🧵 **线程安全** | 注册/注销操作全程加锁保护 |
|
||||||
| 📦 **对象池** | 内置 `EventPool<T>`,高频事件零 GC |
|
| 📦 **对象池** | 内置 `EventPool<T>`,高频事件零 GC |
|
||||||
| 🔍 **调试友好** | Editor 事件查看器实时追踪订阅者与触发日志 |
|
| 🔍 **调试友好** | Editor 事件查看器实时追踪订阅者与触发日志 |
|
||||||
|
| 🧩 **可实例化总线** | 除默认静态 `EventBus` 外,也可以用 Builder 创建独立 bus,并按需要配置异常策略、事件类型约束、分 phase 分发 |
|
||||||
|
| 🌳 **父事件监听** | 监听父事件类型时,子事件触发也会命中父事件监听器,便于做 Pre/Post 家族事件和统一监控 |
|
||||||
|
|
||||||
## 👓 Benchmark
|
## 👓 Benchmark
|
||||||
|
|
||||||
[Benchmark结果](Benchmark.txt)
|
[Benchmark结果](Benchmark.txt)
|
||||||
|
|
||||||
|
运行时仓库里自带 `EventBusBenchmark` 组件,当前会分别覆盖这些场景:
|
||||||
|
|
||||||
|
- 无订阅者 / 单订阅者 / 多订阅者同步触发
|
||||||
|
- 父事件监听子事件
|
||||||
|
- 按 `EventPriority` 分 phase 分发
|
||||||
|
- 单订阅者异步触发
|
||||||
|
- 手工 delegate 注册 / 注销
|
||||||
|
- `object / Type / MethodInfo` 扫描注册 / 注销
|
||||||
|
- `EventPool<T>` 与 `new`
|
||||||
|
- 已取消事件跳过
|
||||||
|
|
||||||
|
如果你在评估这次 `IShrinkEventBus`、继承监听和严格注册带来的成本变化,优先看这个组件的输出,而不是只看 `Benchmark.txt` 里的旧样本。
|
||||||
|
|
||||||
## 📦 依赖
|
## 📦 依赖
|
||||||
|
|
||||||
- Unity 2022.3+
|
- Unity 2022.3+
|
||||||
@@ -43,6 +59,8 @@
|
|||||||
https://github.com/cneicy/ShrinkEventBus.git
|
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<TMarker>()`
|
||||||
|
- `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` 枚举定义了六个优先级档位,数值越小越先执行:
|
`EventPriority` 枚举定义了六个优先级档位,数值越小越先执行:
|
||||||
@@ -167,7 +242,7 @@ HIGHEST(0) → HIGH(1) → NORMAL(2) → LOW(3) → LOWEST(4) → MONITOR(5)
|
|||||||
[EventSubscribe(EventPriority.HIGH)]
|
[EventSubscribe(EventPriority.HIGH)]
|
||||||
private void Handler(SomeEvent evt) { }
|
private void Handler(SomeEvent evt) { }
|
||||||
|
|
||||||
// 数字优先级(自动映射到枚举档位)
|
// 数字优先级(自动映射到枚举档位;手动注册时必须显式传入数字)
|
||||||
EventBus.RegisterEvent<SomeEvent>(Handler, priority: 75); // 映射为 HIGH
|
EventBus.RegisterEvent<SomeEvent>(Handler, priority: 75); // 映射为 HIGH
|
||||||
|
|
||||||
// 手动注册时混合使用
|
// 手动注册时混合使用
|
||||||
@@ -180,10 +255,12 @@ EventBus.RegisterEvent<SomeEvent>(Handler, EventPriority.HIGH, receiveCanceled:
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| ≥ 100 | HIGHEST |
|
| ≥ 100 | HIGHEST |
|
||||||
| ≥ 50 | HIGH |
|
| ≥ 50 | HIGH |
|
||||||
| > 0 | NORMAL |
|
| ≥ 0 | NORMAL |
|
||||||
| ≥ -50 | LOW |
|
| ≥ -50 | LOW |
|
||||||
| < -50 | LOWEST |
|
| < -50 | LOWEST |
|
||||||
|
|
||||||
|
> 1.3.0 起:数字 `0` 映射到 `NORMAL`(与枚举重载默认值一致);int 重载不再提供默认值,不带优先级的 `RegisterEvent(handler)` 调用唯一解析到枚举重载(NORMAL)。
|
||||||
|
|
||||||
**推荐的优先级分工:**
|
**推荐的优先级分工:**
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -195,6 +272,36 @@ LOWEST — 收尾清理
|
|||||||
MONITOR — 日志、统计、监控(通常配合 receiveCanceled: true)
|
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<DamageEvent>(OnAnyDamage, EventPriority.MONITOR, receiveCanceled: true);
|
||||||
|
EventBus.TriggerEvent(new CriticalDamageEvent { Value = 42, IsCritical = true });
|
||||||
|
```
|
||||||
|
|
||||||
|
这很适合做统一日志、统一权限检查、事件族级别的监控和桥接。
|
||||||
|
|
||||||
### 事件取消与结果
|
### 事件取消与结果
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -244,7 +351,8 @@ bool success = pickupEvent.Result switch
|
|||||||
| 方式 | 适用场景 | 自动反注册 |
|
| 方式 | 适用场景 | 自动反注册 |
|
||||||
|------|---------|-----------|
|
|------|---------|-----------|
|
||||||
| `[EventBusSubscriber]` + `[EventSubscribe]` | MonoBehaviour(推荐) | ✅ ILP 织入 OnDestroy,随 GameObject 销毁自动清理 |
|
| `[EventBusSubscriber]` + `[EventSubscribe]` | MonoBehaviour(推荐) | ✅ ILP 织入 OnDestroy,随 GameObject 销毁自动清理 |
|
||||||
| `EventBus.RegisterEvent(...)` 手动注册 | 非 MonoBehaviour 类、Lambda | ❌ 需手动调用 `UnregisterEvent` |
|
| `EventBus.SubscribeEvent(...)` 手动订阅 | 非 MonoBehaviour 类、Lambda | ✅ `Dispose()` 即可精准清理 |
|
||||||
|
| `EventBus.RegisterEvent(...)` 手动注册 | 兼容旧代码 | ❌ 需手动调用 `UnregisterEvent` |
|
||||||
| `EventBus.AutoRegister(this)` | 特殊场景下手动触发 | ❌ 需手动调用 `UnregisterInstance` |
|
| `EventBus.AutoRegister(this)` | 特殊场景下手动触发 | ❌ 需手动调用 `UnregisterInstance` |
|
||||||
|
|
||||||
**手动注册示例(非 MonoBehaviour):**
|
**手动注册示例(非 MonoBehaviour):**
|
||||||
@@ -252,17 +360,18 @@ bool success = pickupEvent.Result switch
|
|||||||
```csharp
|
```csharp
|
||||||
public class InventorySystem : IDisposable
|
public class InventorySystem : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly IShrinkEventSubscription _itemPickupSubscription;
|
||||||
|
|
||||||
public InventorySystem()
|
public InventorySystem()
|
||||||
{
|
{
|
||||||
EventBus.RegisterEvent<ItemPickupEvent>(OnItemPickup, EventPriority.NORMAL);
|
_itemPickupSubscription = EventBus.SubscribeEvent<ItemPickupEvent>(OnItemPickup, EventPriority.NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnItemPickup(ItemPickupEvent evt) { /* ... */ }
|
private void OnItemPickup(ItemPickupEvent evt) { /* ... */ }
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// 必须手动清理,否则 handler 持有 this 引用会造成内存泄漏
|
_itemPickupSubscription.Dispose();
|
||||||
EventBus.UnregisterAllEventsForObject(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -279,9 +388,12 @@ public class InventorySystem : IDisposable
|
|||||||
// 同步 handler
|
// 同步 handler
|
||||||
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
|
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
|
||||||
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, int priority);
|
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, int priority);
|
||||||
|
EventBus.SubscribeEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
|
||||||
|
EventBus.SubscribeEvent<TEvent>(Action<TEvent> handler, int priority);
|
||||||
|
|
||||||
// 异步 handler(UniTask)
|
// 异步 handler(UniTask)
|
||||||
EventBus.RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
|
EventBus.RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
|
||||||
|
EventBus.SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
|
||||||
|
|
||||||
// 注销
|
// 注销
|
||||||
EventBus.UnregisterEvent<TEvent>(Action<TEvent> handler);
|
EventBus.UnregisterEvent<TEvent>(Action<TEvent> handler);
|
||||||
@@ -311,6 +423,7 @@ EventBus.GetRegisteredInstanceCount();
|
|||||||
EventBus.GetRegisteredEventTypeCount();
|
EventBus.GetRegisteredEventTypeCount();
|
||||||
EventBus.GetEventSubscribers<TEvent>(); // 返回 EventHandlerInfo[]
|
EventBus.GetEventSubscribers<TEvent>(); // 返回 EventHandlerInfo[]
|
||||||
EventBus.GetListenerList<TEvent>(); // 无订阅者时返回 null
|
EventBus.GetListenerList<TEvent>(); // 无订阅者时返回 null
|
||||||
|
EventBus.GetActiveSubscriptionsSnapshot();// 返回 IDisposable 订阅快照
|
||||||
```
|
```
|
||||||
|
|
||||||
### EventPool\<T\>
|
### EventPool\<T\>
|
||||||
@@ -333,7 +446,7 @@ EventBus.TriggerEvent(evt);
|
|||||||
### EventBase 关键成员
|
### EventBase 关键成员
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
evt.EventId // Guid,每次触发唯一
|
evt.EventId // Guid,每次派发唯一(懒生成,首次访问时分配)
|
||||||
evt.EventTime // 事件创建时间(UTC)
|
evt.EventTime // 事件创建时间(UTC)
|
||||||
evt.IsCancelable // 是否支持取消(由 [Cancelable] 决定)
|
evt.IsCancelable // 是否支持取消(由 [Cancelable] 决定)
|
||||||
evt.HasResult // 是否支持结果(由 [HasResult] 决定)
|
evt.HasResult // 是否支持结果(由 [HasResult] 决定)
|
||||||
@@ -341,7 +454,7 @@ evt.IsCanceled // 是否已被取消
|
|||||||
evt.Result // 当前结果(EventResult 枚举)
|
evt.Result // 当前结果(EventResult 枚举)
|
||||||
evt.Phase // 当前执行到的优先级阶段
|
evt.Phase // 当前执行到的优先级阶段
|
||||||
evt.CurrentHandler // 当前正在执行的 handler 信息
|
evt.CurrentHandler // 当前正在执行的 handler 信息
|
||||||
evt.GetSubscribers() // 获取 handler 列表快照(调试用)
|
evt.GetSubscribers() // 获取本次派发的 handler 快照拷贝(调试用)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -351,46 +464,51 @@ evt.GetSubscribers() // 获取 handler 列表快照(调试用)
|
|||||||
```
|
```
|
||||||
ShrinkEventBus
|
ShrinkEventBus
|
||||||
├── Runtime/
|
├── Runtime/
|
||||||
│ ├── EventBus 静态门面,所有公开 API 的入口
|
│ ├── EventBus 静态门面,内部是一个默认 IShrinkEventBus 实例
|
||||||
│ ├── EventCache<T> 泛型静态缓存,热路径绕过字典查找
|
│ ├── ShrinkEventBusInstance 总线实现:注册、派发、异常策略、phase 分发
|
||||||
│ ├── ListenerList 线程安全的有序 handler 列表
|
│ ├── ShrinkEventBusBuilder 实例总线的构建与配置入口
|
||||||
│ ├── EventHandlerInfo 单个 handler 的元信息(优先级、方法反射、调试信息)
|
│ ├── ListenerList 按 phase 分桶的有序 handler 列表,带快照缓存与父链合并
|
||||||
│ ├── EventBase 所有事件的基类,携带生命周期状态
|
│ ├── EventHandlerInfo 单个 handler 的元信息(优先级、预编译 invoker、调试信息)
|
||||||
|
│ ├── EventBase 所有事件的基类,携带生命周期状态与派发快照
|
||||||
│ ├── EventPool<T> 对象池,高频事件减少 GC
|
│ ├── EventPool<T> 对象池,高频事件减少 GC
|
||||||
|
│ ├── EventCloneUtility 同步路径上 async handler 的事件快照克隆
|
||||||
│ ├── EventBusRegHelper 反射扫描 & handler 注册逻辑
|
│ ├── EventBusRegHelper 反射扫描 & handler 注册逻辑
|
||||||
│ └── EventAutoRegHelper 运行时初始化,确保 IsInitialized 状态正确
|
│ └── EventAutoRegHelper 运行时初始化,确保 IsInitialized 状态正确
|
||||||
│
|
│
|
||||||
├── Editor/
|
├── Editor/
|
||||||
│ └── EventBusViewerWindow 事件查看器,实时显示订阅者与触发日志
|
│ └── EventBusViewerWindow 事件查看器,实时显示订阅者与触发日志
|
||||||
│
|
│
|
||||||
└── CodeGen/
|
└── (织入)ShrinkShared.CodeGen / CodeGen 共享 ILPostProcessor 优先,本地 ILPostProcessor 兜底
|
||||||
└── EventBusILPostProcessor 编译期织入,向 [EventBusSubscriber] 类注入
|
向 [EventBusSubscriber] 类注入 Awake(AutoRegister)
|
||||||
Awake(AutoRegister)与 OnDestroy(UnregisterInstance)
|
与 OnDestroy(UnregisterInstance)
|
||||||
```
|
```
|
||||||
|
|
||||||
**热路径(`TriggerEvent`)工作流:**
|
**热路径(`TriggerEvent`)工作流:**
|
||||||
|
|
||||||
```
|
```
|
||||||
TriggerEvent(evt)
|
TriggerEvent(evt)
|
||||||
└─ 读取 EventCache<T>.List // 静态字段,O(1),无字典查找
|
└─ 取该事件类型的 ListenerList // 总线级字典 + 共享锁,每类型常数开销
|
||||||
└─ GetHandlers() // 返回内部快照数组,无拷贝
|
└─ GetHandlers() // 返回缓存快照数组(脏时才重建),无拷贝
|
||||||
|
├─ 快照数组引用挂到事件对象上(一次赋值,供 GetSubscribers 调试)
|
||||||
└─ 遍历 handlers[]
|
└─ 遍历 handlers[]
|
||||||
├─ 跳过已取消 & 不接收取消的 handler
|
├─ 跳过已取消 & 不接收取消的 handler
|
||||||
├─ Action<T> → 直接调用
|
├─ Action<T> → 经预编译 invoker 直接调用
|
||||||
└─ Func<T, UniTask> → 基于事件快照 .Forget()(同步路径)
|
└─ Func<T, UniTask> → 克隆事件快照后 .Forget()(同步路径)
|
||||||
```
|
```
|
||||||
|
|
||||||
**自动注册完整流程:**
|
**自动注册完整流程:**
|
||||||
|
|
||||||
```
|
```
|
||||||
【编译期】ILPostProcessor 扫描所有程序集
|
【编译期】若当前程序集命中 ShrinkShared.CodeGen 覆盖范围,则由共享 ILPostProcessor 扫描;
|
||||||
└─ 找到标记了 [EventBusSubscriber] 的 MonoBehaviour 子类
|
否则由 CodeGen/EventBusILPostProcessor.cs 本地扫描
|
||||||
|
└─ 找到标记了 [EventBusSubscriber] 且存在实例 [EventSubscribe] 方法的 MonoBehaviour 子类
|
||||||
├─ 在 Awake 头部织入 EventBus.AutoRegister(this)
|
├─ 在 Awake 头部织入 EventBus.AutoRegister(this)
|
||||||
└─ 在 OnDestroy 头部织入 EventBus.UnregisterInstance(this)
|
└─ 在 OnDestroy 头部织入 EventBus.UnregisterInstance(this)
|
||||||
(类无对应方法时自动生成,有虚基类方法时自动调用 base)
|
(类无对应方法时自动生成,有虚基类方法时自动调用 base)
|
||||||
|
※ 只引用 ShrinkEventBus.Runtime 的纯业务程序集目前不在织入范围内,需手动 AutoRegister
|
||||||
|
|
||||||
【运行时 - 场景加载】RuntimeInitializeOnLoadMethod(AfterSceneLoad)
|
【运行时 - 默认静态总线启动】
|
||||||
└─ 扫描程序集中的 [EventBusSubscriber] 类型,标记 IsInitialized
|
└─ 读取编译期静态订阅清单,注册 static [EventSubscribe] 方法
|
||||||
|
|
||||||
【运行时 - 动态创建】Instantiate(prefab)
|
【运行时 - 动态创建】Instantiate(prefab)
|
||||||
└─ Unity 调用新对象的 Awake(已含织入代码)→ 自动注册
|
└─ Unity 调用新对象的 Awake(已含织入代码)→ 自动注册
|
||||||
@@ -460,6 +578,8 @@ public void Dispose()
|
|||||||
- **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 快照,会产生语义上的不确定性。
|
- **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 快照,会产生语义上的不确定性。
|
||||||
- **静态 handler 永远不会自动注销**:静态方法注册后持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。
|
- **静态 handler 永远不会自动注销**:静态方法注册后持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。
|
||||||
- **`[EventBusSubscriber]` 仅对 MonoBehaviour 生效自动注册**:非 MonoBehaviour 类标记该 Attribute 无任何效果,请使用手动注册。
|
- **`[EventBusSubscriber]` 仅对 MonoBehaviour 生效自动注册**:非 MonoBehaviour 类标记该 Attribute 无任何效果,请使用手动注册。
|
||||||
|
- **标了 `[EventBusSubscriber]` 但没有实例 `[EventSubscribe]` 方法的类**:编译期不会织入;若通过 `AutoRegister` 手动接入,会输出警告并跳过(不抛异常)。显式 `Register()` 对此仍严格抛错。
|
||||||
|
- **int 数字优先级重载必须显式传值**:1.3.0 起 int 重载不再有默认值;数字 `0` 映射 `NORMAL`。
|
||||||
- **ILPostProcessor 织入发生在编译期**:修改代码后需要重新编译才能使注入生效,热重载场景下请注意这一点。
|
- **ILPostProcessor 织入发生在编译期**:修改代码后需要重新编译才能使注入生效,热重载场景下请注意这一点。
|
||||||
- **继承泛型基类(如 `Singleton<T>`)时无需额外处理**:ILP 会正确识别泛型基类中的虚方法并生成 `protected override`,自动调用 `base.Awake()` 和 `base.OnDestroy()`。
|
- **继承泛型基类(如 `Singleton<T>`)时无需额外处理**:ILP 会正确识别泛型基类中的虚方法并生成 `protected override`,自动调用 `base.Awake()` 和 `base.OnDestroy()`。
|
||||||
|
|
||||||
@@ -470,7 +590,7 @@ public void Dispose()
|
|||||||
**事件没有被任何 handler 接收**
|
**事件没有被任何 handler 接收**
|
||||||
|
|
||||||
1. 检查订阅类是否有 `[EventBusSubscriber]`
|
1. 检查订阅类是否有 `[EventBusSubscriber]`
|
||||||
2. 检查方法是否有 `[EventSubscribe]`,且签名为 `void/UniTask/Task Method(TEvent evt)`
|
2. 检查方法是否有 `[EventSubscribe]`,且签名为 `void/UniTask Method(TEvent evt)`
|
||||||
3. 确认代码在标记 `[EventBusSubscriber]` 后重新编译过(ILPostProcessor 需要编译期运行)
|
3. 确认代码在标记 `[EventBusSubscriber]` 后重新编译过(ILPostProcessor 需要编译期运行)
|
||||||
4. 确认没有在 `Awake` 之前就触发事件
|
4. 确认没有在 `Awake` 之前就触发事件
|
||||||
|
|
||||||
|
|||||||
+54
-15
@@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
@@ -7,7 +8,16 @@ namespace ShrinkEventBus
|
|||||||
{
|
{
|
||||||
public abstract class EventBase : IDisposable
|
public abstract class EventBase : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ListenerList _listenerList = new();
|
private sealed class EventMetadata
|
||||||
|
{
|
||||||
|
public bool IsCancelable;
|
||||||
|
public bool HasResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<Type, EventMetadata> MetadataCache = new();
|
||||||
|
|
||||||
|
private EventHandlerInfo[]? _dispatchSnapshot;
|
||||||
|
private Guid? _eventId;
|
||||||
private bool _isCanceled;
|
private bool _isCanceled;
|
||||||
private EventResult _result = EventResult.DEFAULT;
|
private EventResult _result = EventResult.DEFAULT;
|
||||||
|
|
||||||
@@ -18,7 +28,7 @@ namespace ShrinkEventBus
|
|||||||
public DateTime EventTime { get; private set; } = DateTime.UtcNow;
|
public DateTime EventTime { get; private set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
public Guid EventId { get; private set; } = Guid.NewGuid();
|
public Guid EventId => _eventId ??= Guid.NewGuid();
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
public bool IsCancelable { get; }
|
public bool IsCancelable { get; }
|
||||||
@@ -34,13 +44,14 @@ namespace ShrinkEventBus
|
|||||||
|
|
||||||
protected EventBase()
|
protected EventBase()
|
||||||
{
|
{
|
||||||
IsCancelable = GetType().GetCustomAttribute<CancelableAttribute>() != null;
|
var metadata = GetOrCreateMetadata(GetType());
|
||||||
HasResult = GetType().GetCustomAttribute<HasResultAttribute>() != null;
|
IsCancelable = metadata.IsCancelable;
|
||||||
|
HasResult = metadata.HasResult;
|
||||||
Setup();
|
Setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Setup() { }
|
protected virtual void Setup() { }
|
||||||
|
|
||||||
protected virtual void OnReset() { }
|
protected virtual void OnReset() { }
|
||||||
|
|
||||||
internal void ResetInternal()
|
internal void ResetInternal()
|
||||||
@@ -50,8 +61,8 @@ namespace ShrinkEventBus
|
|||||||
CurrentHandler = null;
|
CurrentHandler = null;
|
||||||
Phase = null;
|
Phase = null;
|
||||||
EventTime = DateTime.UtcNow;
|
EventTime = DateTime.UtcNow;
|
||||||
EventId = Guid.NewGuid();
|
_eventId = null;
|
||||||
_listenerList.Clear();
|
_dispatchSnapshot = null;
|
||||||
OnReset();
|
OnReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +71,13 @@ namespace ShrinkEventBus
|
|||||||
CurrentHandler = null;
|
CurrentHandler = null;
|
||||||
Phase = null;
|
Phase = null;
|
||||||
EventTime = DateTime.UtcNow;
|
EventTime = DateTime.UtcNow;
|
||||||
EventId = Guid.NewGuid();
|
_eventId = null;
|
||||||
_listenerList.Clear();
|
_dispatchSnapshot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetListenerSnapshot(EventHandlerInfo[]? handlers)
|
||||||
|
{
|
||||||
|
_dispatchSnapshot = handlers is { Length: > 0 } ? handlers : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
@@ -70,7 +86,9 @@ namespace ShrinkEventBus
|
|||||||
get => _isCanceled;
|
get => _isCanceled;
|
||||||
set
|
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;
|
_isCanceled = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +99,9 @@ namespace ShrinkEventBus
|
|||||||
get => _result;
|
get => _result;
|
||||||
set
|
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;
|
_result = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,18 +110,37 @@ namespace ShrinkEventBus
|
|||||||
{
|
{
|
||||||
if (Phase == value) return;
|
if (Phase == value) return;
|
||||||
if (Phase != null && Phase.Value.CompareTo(value) > 0)
|
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;
|
Phase = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCanceled(bool canceled) => IsCanceled = canceled;
|
public void SetCanceled(bool canceled) => IsCanceled = canceled;
|
||||||
public void SetResult(EventResult result) => Result = result;
|
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<EventHandlerInfo>();
|
||||||
|
|
||||||
|
var copy = new EventHandlerInfo[snapshot.Length];
|
||||||
|
Array.Copy(snapshot, copy, snapshot.Length);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
ReleaseAction?.Invoke(this);
|
ReleaseAction?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static EventMetadata GetOrCreateMetadata(Type eventType)
|
||||||
|
{
|
||||||
|
return MetadataCache.GetOrAdd(eventType, static type => new EventMetadata
|
||||||
|
{
|
||||||
|
IsCancelable = type.GetCustomAttribute<CancelableAttribute>() != null,
|
||||||
|
HasResult = type.GetCustomAttribute<HasResultAttribute>() != null
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+105
-374
@@ -1,398 +1,129 @@
|
|||||||
using System;
|
#nullable enable
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace ShrinkEventBus
|
namespace ShrinkEventBus
|
||||||
{
|
{
|
||||||
public static class EventBus
|
public static class EventBus
|
||||||
{
|
{
|
||||||
public static event Action<EventBase, Type> OnEventTriggered;
|
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
|
||||||
|
|
||||||
|
public static IShrinkEventBus Default => DefaultBus;
|
||||||
|
|
||||||
|
public static event Action<EventBase, Type> OnEventTriggered
|
||||||
|
{
|
||||||
|
add => DefaultBus.OnEventTriggered += value;
|
||||||
|
remove => DefaultBus.OnEventTriggered -= value;
|
||||||
|
}
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
public static bool EnableDebugRecord { get; set; }
|
public static bool EnableDebugRecord
|
||||||
public static event Action<EventBase, string, string> OnEventTriggeredForEditor;
|
{
|
||||||
|
get => DefaultBus.EnableDebugRecord;
|
||||||
|
set => DefaultBus.EnableDebugRecord = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static event Action<EventBase, string, string, EventHandlerInfo[]> OnEventTriggeredForEditor
|
||||||
|
{
|
||||||
|
add => DefaultBus.OnEventTriggeredForEditor += value;
|
||||||
|
remove => DefaultBus.OnEventTriggeredForEditor -= value;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private static readonly Dictionary<Type, ListenerList> EventHandlers = new();
|
public static ShrinkEventBusBuilder Builder() => new();
|
||||||
private static readonly HashSet<object> RegisteredInstances = new();
|
|
||||||
private static readonly object InstanceLock = new();
|
|
||||||
|
|
||||||
static EventBus()
|
public static IShrinkEventBus CreateBus(Action<ShrinkEventBusBuilder>? configure = null)
|
||||||
{
|
{
|
||||||
EventBusRegHelper.RegStaticEventHandler();
|
var builder = new ShrinkEventBusBuilder();
|
||||||
|
configure?.Invoke(builder);
|
||||||
|
return builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
public static void Start() => DefaultBus.Start();
|
||||||
private static void EnsureAutoManagerInitialized() => EventAutoRegHelper.EnsureInitialized();
|
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<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||||
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority = 0)
|
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, 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 static void RegisterEvent<TEvent>(Action<TEvent> handler,
|
||||||
public static void RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority = EventPriority.NORMAL,
|
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||||
bool receiveCanceled = false) where TEvent : EventBase
|
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled);
|
||||||
{
|
|
||||||
RegisterEventInternal(typeof(TEvent), handler, priority, 0, receiveCanceled,
|
|
||||||
$"Manual Sync Handler (Priority: {priority})", handler.Method);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
public static void RegisterEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||||
public static void RegisterEvent<TEvent>(Action<TEvent> handler, int priority = 0) where TEvent : EventBase
|
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
|
||||||
{
|
|
||||||
var eventPriority = PriorityHelper.ConvertToEventPriority(priority);
|
|
||||||
RegisterEventInternal(typeof(TEvent), handler, eventPriority, priority, false,
|
|
||||||
$"Manual Sync Handler (Priority: {priority})", handler.Method);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler,
|
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> 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<TEvent>(Action<TEvent> handler,
|
||||||
|
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||||
|
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled);
|
||||||
|
|
||||||
|
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||||
|
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority);
|
||||||
|
|
||||||
|
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
|
||||||
|
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||||
|
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled);
|
||||||
|
|
||||||
|
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||||
|
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority);
|
||||||
|
|
||||||
|
public static void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase =>
|
||||||
|
DefaultBus.UnregisterEvent(handler);
|
||||||
|
|
||||||
|
public static void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase =>
|
||||||
|
DefaultBus.UnregisterEvent(handler);
|
||||||
|
|
||||||
|
public static void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase =>
|
||||||
|
DefaultBus.ClearAllSubscribersForEvent<TEvent>();
|
||||||
|
|
||||||
|
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<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase =>
|
||||||
|
DefaultBus.TriggerEventAsync(eventArgs);
|
||||||
|
|
||||||
|
public static UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs)
|
||||||
|
where TEvent : EventBase => DefaultBus.TriggerEventAsync(phase, eventArgs);
|
||||||
|
|
||||||
|
public static bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase =>
|
||||||
|
DefaultBus.TriggerEvent(eventArgs);
|
||||||
|
|
||||||
|
public static bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase =>
|
||||||
|
DefaultBus.TriggerEvent(phase, eventArgs);
|
||||||
|
|
||||||
|
public static EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase =>
|
||||||
|
DefaultBus.GetEventSubscribers<TEvent>();
|
||||||
|
|
||||||
|
public static ListenerList GetListenerList<TEvent>() where TEvent : EventBase =>
|
||||||
|
DefaultBus.GetListenerList<TEvent>();
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot() =>
|
||||||
|
DefaultBus.GetAllSubscribersSnapshot();
|
||||||
|
|
||||||
|
public static IReadOnlyList<ShrinkEventSubscriptionSnapshot> 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,
|
var bus = (ShrinkEventBusInstance)new ShrinkEventBusBuilder()
|
||||||
$"Manual Async Handler (Priority: {priority})");
|
.SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndContinue)
|
||||||
}
|
.AllowPerPhaseDispatch()
|
||||||
|
.Build();
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
EventBusRegHelper.RegStaticEventHandler(bus);
|
||||||
internal static void RegisterEventInternal(Type eventType, Delegate handler, EventPriority priority,
|
return bus;
|
||||||
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<TEvent>(Func<TEvent, UniTask> 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<TEvent>.List = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static void UnregisterEvent<TEvent>(Action<TEvent> 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<TEvent>.List = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase
|
|
||||||
{
|
|
||||||
var eventType = typeof(TEvent);
|
|
||||||
lock (EventHandlers)
|
|
||||||
{
|
|
||||||
if (EventHandlers.Remove(eventType))
|
|
||||||
EventCache<TEvent>.List = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static void UnregisterAllEventsForObject(object targetObject)
|
|
||||||
{
|
|
||||||
if (targetObject is null) return;
|
|
||||||
lock (EventHandlers)
|
|
||||||
{
|
|
||||||
var eventTypesToRemove = new List<Type>();
|
|
||||||
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>(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<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase
|
|
||||||
{
|
|
||||||
EnsureAutoManagerInitialized();
|
|
||||||
eventArgs.PrepareForDispatch();
|
|
||||||
var collection = EventCache<TEvent>.List;
|
|
||||||
var handlers = collection?.GetHandlers() ?? Array.Empty<EventHandlerInfo>();
|
|
||||||
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<TEvent> syncHandler)
|
|
||||||
{
|
|
||||||
syncHandler(eventArgs);
|
|
||||||
wasHandled = true;
|
|
||||||
}
|
|
||||||
else if (handlerInfo.Handler is Func<TEvent, UniTask> 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>(TEvent eventArgs) where TEvent : EventBase
|
|
||||||
{
|
|
||||||
EnsureAutoManagerInitialized();
|
|
||||||
eventArgs.PrepareForDispatch();
|
|
||||||
var collection = EventCache<TEvent>.List;
|
|
||||||
var handlers = collection?.GetHandlers() ?? Array.Empty<EventHandlerInfo>();
|
|
||||||
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<TEvent> syncHandler)
|
|
||||||
{
|
|
||||||
syncHandler(eventArgs);
|
|
||||||
wasHandled = true;
|
|
||||||
}
|
|
||||||
else if (handlerInfo.Handler is Func<TEvent, UniTask> 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<UniTask> 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<TEvent>() where TEvent : EventBase
|
|
||||||
{
|
|
||||||
var eventType = typeof(TEvent);
|
|
||||||
lock (EventHandlers)
|
|
||||||
{
|
|
||||||
return EventHandlers.TryGetValue(eventType, out var collection)
|
|
||||||
? collection.GetHandlers()
|
|
||||||
: Array.Empty<EventHandlerInfo>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ListenerList GetListenerList<TEvent>() 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+274
-75
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -8,39 +9,98 @@ using Debug = UnityEngine.Debug;
|
|||||||
namespace ShrinkEventBus.Runtime
|
namespace ShrinkEventBus.Runtime
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ShrinkEventBus 性能测试
|
/// ShrinkEventBus 性能测试。
|
||||||
/// 挂在任意 GameObject 上,运行后在 Console 查看结果。
|
/// 挂在任意 GameObject 上,运行后在 Console 查看结果。
|
||||||
/// 在进行测试时不要打开事件查看器大的追踪实时事件功能
|
/// 在进行测试时不要打开事件查看器的大量实时追踪。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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;
|
[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 registerIterations = 5_000;
|
||||||
|
public int scannedRegisterIterations = 1_000;
|
||||||
[Header("取消事件测试")] public int cancelIterations = 100000;
|
|
||||||
|
|
||||||
|
[Header("取消事件测试")] public int cancelIterations = 100_000;
|
||||||
|
|
||||||
public class BenchmarkEvent : EventBase
|
public class BenchmarkEvent : EventBase
|
||||||
{
|
{
|
||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class DerivedBenchmarkEvent : BenchmarkEvent
|
||||||
|
{
|
||||||
|
public bool IsDerived { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[Cancelable]
|
[Cancelable]
|
||||||
public class CancelableBenchmarkEvent : EventBase
|
public class CancelableBenchmarkEvent : EventBase
|
||||||
{
|
{
|
||||||
public int Value { get; set; }
|
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 readonly StringBuilder _report = new();
|
||||||
|
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
RunAllBenchmarks().Forget();
|
RunAllBenchmarks().Forget();
|
||||||
@@ -52,8 +112,9 @@ namespace ShrinkEventBus.Runtime
|
|||||||
_report.AppendLine("╔══════════════════════════════════════════════════╗");
|
_report.AppendLine("╔══════════════════════════════════════════════════╗");
|
||||||
_report.AppendLine("║ ShrinkEventBus Benchmark Report ║");
|
_report.AppendLine("║ ShrinkEventBus Benchmark Report ║");
|
||||||
_report.AppendLine(
|
_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("╚══════════════════════════════════════════════════╝");
|
||||||
|
_report.AppendLine(" 基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启)");
|
||||||
|
|
||||||
Log("开始:无订阅者触发");
|
Log("开始:无订阅者触发");
|
||||||
Bench_TriggerEvent_NoSubscriber();
|
Bench_TriggerEvent_NoSubscriber();
|
||||||
@@ -63,16 +124,36 @@ namespace ShrinkEventBus.Runtime
|
|||||||
Bench_TriggerEvent_SingleSubscriber();
|
Bench_TriggerEvent_SingleSubscriber();
|
||||||
await UniTask.Yield();
|
await UniTask.Yield();
|
||||||
|
|
||||||
|
Log("开始:父事件监听子事件");
|
||||||
|
Bench_TriggerEvent_InheritedSubscriber();
|
||||||
|
await UniTask.Yield();
|
||||||
|
|
||||||
Log($"开始:{subscriberCount} 订阅者同步触发");
|
Log($"开始:{subscriberCount} 订阅者同步触发");
|
||||||
Bench_TriggerEvent_MassSubscribers();
|
Bench_TriggerEvent_MassSubscribers();
|
||||||
await UniTask.Yield();
|
await UniTask.Yield();
|
||||||
|
|
||||||
|
Log("开始:按 phase 分发");
|
||||||
|
Bench_TriggerEvent_PerPhase();
|
||||||
|
await UniTask.Yield();
|
||||||
|
|
||||||
Log("开始:异步触发");
|
Log("开始:异步触发");
|
||||||
await Bench_TriggerEventAsync();
|
await Bench_TriggerEventAsync();
|
||||||
await UniTask.Yield();
|
await UniTask.Yield();
|
||||||
|
|
||||||
Log("开始:注册/注销");
|
Log("开始:手工 delegate 注册/注销");
|
||||||
Bench_RegisterUnregister();
|
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();
|
await UniTask.Yield();
|
||||||
|
|
||||||
Log("开始:EventPool vs new");
|
Log("开始:EventPool vs new");
|
||||||
@@ -87,95 +168,141 @@ namespace ShrinkEventBus.Runtime
|
|||||||
Debug.Log(_report.ToString());
|
Debug.Log(_report.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Bench_TriggerEvent_NoSubscriber()
|
private void Bench_TriggerEvent_NoSubscriber()
|
||||||
{
|
{
|
||||||
EventBus.ClearAllSubscribersForEvent<BenchmarkEvent>();
|
var bus = CreateBenchmarkBus();
|
||||||
var evt = new BenchmarkEvent { Value = 1 };
|
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();
|
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();
|
sw.Stop();
|
||||||
|
|
||||||
Record("TriggerEvent × 无订阅者(基线)", sw.Elapsed.TotalMilliseconds, iterations);
|
Record("TriggerEvent × 无订阅者(基线)", sw.Elapsed.TotalMilliseconds, iterations);
|
||||||
Log("完成:无订阅者触发");
|
Log("完成:无订阅者触发");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Bench_TriggerEvent_SingleSubscriber()
|
private void Bench_TriggerEvent_SingleSubscriber()
|
||||||
{
|
{
|
||||||
EventBus.ClearAllSubscribersForEvent<BenchmarkEvent>();
|
var bus = CreateBenchmarkBus();
|
||||||
|
|
||||||
var dummy = 0;
|
var dummy = 0;
|
||||||
|
|
||||||
void Handler(BenchmarkEvent e)
|
void Handler(BenchmarkEvent evt)
|
||||||
{
|
{
|
||||||
dummy = e.Value;
|
dummy = evt.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventBus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||||
|
|
||||||
var evt = new BenchmarkEvent { Value = 1 };
|
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();
|
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();
|
sw.Stop();
|
||||||
|
|
||||||
Record("TriggerEvent × 单订阅者同步", sw.Elapsed.TotalMilliseconds, iterations);
|
Record("TriggerEvent × 单订阅者同步", sw.Elapsed.TotalMilliseconds, iterations);
|
||||||
Log("完成:单订阅者同步触发");
|
Log("完成:单订阅者同步触发");
|
||||||
|
|
||||||
EventBus.UnregisterEvent<BenchmarkEvent>(Handler);
|
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||||
GC.KeepAlive(dummy);
|
GC.KeepAlive(dummy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Bench_TriggerEvent_InheritedSubscriber()
|
||||||
|
{
|
||||||
|
var bus = CreateBenchmarkBus();
|
||||||
|
var dummy = 0;
|
||||||
|
|
||||||
|
void Handler(BenchmarkEvent evt)
|
||||||
|
{
|
||||||
|
dummy = evt.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.RegisterEvent<BenchmarkEvent>(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<BenchmarkEvent>(Handler);
|
||||||
|
GC.KeepAlive(dummy);
|
||||||
|
}
|
||||||
|
|
||||||
private void Bench_TriggerEvent_MassSubscribers()
|
private void Bench_TriggerEvent_MassSubscribers()
|
||||||
{
|
{
|
||||||
EventBus.ClearAllSubscribersForEvent<BenchmarkEvent>();
|
var bus = CreateBenchmarkBus();
|
||||||
|
|
||||||
var dummy = 0;
|
var dummy = 0;
|
||||||
var handlers = new Action<BenchmarkEvent>[subscriberCount];
|
var handlers = new Action<BenchmarkEvent>[subscriberCount];
|
||||||
for (var i = 0; i < subscriberCount; i++)
|
for (var i = 0; i < subscriberCount; i++)
|
||||||
{
|
{
|
||||||
var captured = i;
|
var captured = i;
|
||||||
handlers[i] = e => { dummy = captured + e.Value; };
|
handlers[i] = evt => { dummy = captured + evt.Value; };
|
||||||
EventBus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
var evt = new BenchmarkEvent { Value = 1 };
|
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();
|
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();
|
sw.Stop();
|
||||||
|
|
||||||
Record($"TriggerEvent × {subscriberCount} 订阅者同步", sw.Elapsed.TotalMilliseconds, massIterations);
|
Record($"TriggerEvent × {subscriberCount} 订阅者同步", sw.Elapsed.TotalMilliseconds, massIterations);
|
||||||
Log($"完成:{subscriberCount} 订阅者同步触发");
|
Log($"完成:{subscriberCount} 订阅者同步触发");
|
||||||
|
|
||||||
foreach (var h in handlers) EventBus.UnregisterEvent<BenchmarkEvent>(h);
|
foreach (var handler in handlers)
|
||||||
|
bus.UnregisterEvent<BenchmarkEvent>(handler);
|
||||||
|
|
||||||
GC.KeepAlive(dummy);
|
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()
|
private async UniTask Bench_TriggerEventAsync()
|
||||||
{
|
{
|
||||||
EventBus.ClearAllSubscribersForEvent<BenchmarkEvent>();
|
var bus = CreateBenchmarkBus();
|
||||||
|
|
||||||
var dummy = 0;
|
var dummy = 0;
|
||||||
|
|
||||||
async UniTask Handler(BenchmarkEvent e)
|
async UniTask Handler(BenchmarkEvent evt)
|
||||||
{
|
{
|
||||||
dummy = e.Value;
|
dummy = evt.Value;
|
||||||
await UniTask.CompletedTask;
|
await UniTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventBus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||||
|
|
||||||
var evt = new BenchmarkEvent { Value = 1 };
|
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;
|
const int perFrame = 10;
|
||||||
var totalMs = 0.0;
|
var totalMs = 0.0;
|
||||||
@@ -186,7 +313,8 @@ namespace ShrinkEventBus.Runtime
|
|||||||
await UniTask.Yield();
|
await UniTask.Yield();
|
||||||
var count = Math.Min(perFrame, asyncIterations - ran);
|
var count = Math.Min(perFrame, asyncIterations - ran);
|
||||||
var sw = Stopwatch.StartNew();
|
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();
|
sw.Stop();
|
||||||
totalMs += sw.Elapsed.TotalMilliseconds;
|
totalMs += sw.Elapsed.TotalMilliseconds;
|
||||||
ran += count;
|
ran += count;
|
||||||
@@ -195,48 +323,102 @@ namespace ShrinkEventBus.Runtime
|
|||||||
Record("TriggerEventAsync × 单订阅者异步", totalMs, asyncIterations);
|
Record("TriggerEventAsync × 单订阅者异步", totalMs, asyncIterations);
|
||||||
Log("完成:异步触发");
|
Log("完成:异步触发");
|
||||||
|
|
||||||
EventBus.UnregisterEvent<BenchmarkEvent>(Handler);
|
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||||
GC.KeepAlive(dummy);
|
GC.KeepAlive(dummy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Bench_RegisterUnregister_ManualDelegate()
|
||||||
private void Bench_RegisterUnregister()
|
|
||||||
{
|
{
|
||||||
|
var bus = CreateBenchmarkBus();
|
||||||
var dummy = 0;
|
var dummy = 0;
|
||||||
var handlers = new Action<BenchmarkEvent>[registerIterations];
|
var handlers = new Action<BenchmarkEvent>[registerIterations];
|
||||||
for (var i = 0; i < registerIterations; i++)
|
for (var i = 0; i < registerIterations; i++)
|
||||||
{
|
{
|
||||||
var captured = i;
|
var captured = i;
|
||||||
handlers[i] = e => { dummy = captured; };
|
handlers[i] = evt => { dummy = captured; };
|
||||||
}
|
}
|
||||||
|
|
||||||
var swReg = Stopwatch.StartNew();
|
var swReg = Stopwatch.StartNew();
|
||||||
for (var i = 0; i < registerIterations; i++)
|
for (var i = 0; i < registerIterations; i++)
|
||||||
EventBus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||||
swReg.Stop();
|
swReg.Stop();
|
||||||
Record("RegisterEvent", swReg.Elapsed.TotalMilliseconds, registerIterations);
|
Record("RegisterEvent(delegate)", swReg.Elapsed.TotalMilliseconds, registerIterations);
|
||||||
Log("完成:RegisterEvent");
|
|
||||||
|
|
||||||
var swUnreg = Stopwatch.StartNew();
|
var swUnreg = Stopwatch.StartNew();
|
||||||
for (var i = 0; i < registerIterations; i++)
|
for (var i = 0; i < registerIterations; i++)
|
||||||
EventBus.UnregisterEvent<BenchmarkEvent>(handlers[i]);
|
bus.UnregisterEvent<BenchmarkEvent>(handlers[i]);
|
||||||
swUnreg.Stop();
|
swUnreg.Stop();
|
||||||
Record("UnregisterEvent", swUnreg.Elapsed.TotalMilliseconds, registerIterations);
|
Record("UnregisterEvent(delegate)", swUnreg.Elapsed.TotalMilliseconds, registerIterations);
|
||||||
Log("完成:UnregisterEvent");
|
Log("完成:手工 delegate 注册/注销");
|
||||||
|
|
||||||
GC.KeepAlive(dummy);
|
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()
|
private void Bench_EventPool_vs_New()
|
||||||
{
|
{
|
||||||
EventBus.ClearAllSubscribersForEvent<BenchmarkEvent>();
|
var bus = CreateBenchmarkBus();
|
||||||
|
|
||||||
void Handler(BenchmarkEvent e)
|
void Handler(BenchmarkEvent evt)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
EventBus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||||
|
|
||||||
for (var i = 0; i < 64; i++)
|
for (var i = 0; i < 64; i++)
|
||||||
EventPool<BenchmarkEvent>.Release(EventPool<BenchmarkEvent>.Get());
|
EventPool<BenchmarkEvent>.Release(EventPool<BenchmarkEvent>.Get());
|
||||||
@@ -246,75 +428,92 @@ namespace ShrinkEventBus.Runtime
|
|||||||
var swNew = Stopwatch.StartNew();
|
var swNew = Stopwatch.StartNew();
|
||||||
for (var i = 0; i < iterations; i++)
|
for (var i = 0; i < iterations; i++)
|
||||||
{
|
{
|
||||||
var e = new BenchmarkEvent { Value = i };
|
var evt = new BenchmarkEvent { Value = i };
|
||||||
EventBus.TriggerEvent(e);
|
bus.TriggerEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
swNew.Stop();
|
swNew.Stop();
|
||||||
var gcNew = GC.CollectionCount(0) - gcBefore;
|
var gcNew = GC.CollectionCount(0) - gcBefore;
|
||||||
Record($"TriggerEvent × new() [GC Gen0={gcNew,3}]", swNew.Elapsed.TotalMilliseconds, iterations);
|
Record($"TriggerEvent × new() [GC Gen0={gcNew,3}]", swNew.Elapsed.TotalMilliseconds, iterations);
|
||||||
Log("完成:new() 分配");
|
|
||||||
|
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
gcBefore = GC.CollectionCount(0);
|
gcBefore = GC.CollectionCount(0);
|
||||||
var swPool = Stopwatch.StartNew();
|
var swPool = Stopwatch.StartNew();
|
||||||
for (var i = 0; i < iterations; i++)
|
for (var i = 0; i < iterations; i++)
|
||||||
{
|
{
|
||||||
using var e = EventPool<BenchmarkEvent>.Get();
|
using var evt = EventPool<BenchmarkEvent>.Get();
|
||||||
e.Value = i;
|
evt.Value = i;
|
||||||
EventBus.TriggerEvent(e);
|
bus.TriggerEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
swPool.Stop();
|
swPool.Stop();
|
||||||
var gcPool = GC.CollectionCount(0) - gcBefore;
|
var gcPool = GC.CollectionCount(0) - gcBefore;
|
||||||
Record($"TriggerEvent × Pool.Get() [GC Gen0={gcPool,3}]", swPool.Elapsed.TotalMilliseconds, iterations);
|
Record($"TriggerEvent × Pool.Get() [GC Gen0={gcPool,3}]", swPool.Elapsed.TotalMilliseconds, iterations);
|
||||||
Log("完成:Pool.Get()");
|
Log("完成:EventPool vs new");
|
||||||
|
|
||||||
EventBus.UnregisterEvent<BenchmarkEvent>(Handler);
|
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Bench_CanceledEvent_Skip()
|
private void Bench_CanceledEvent_Skip()
|
||||||
{
|
{
|
||||||
EventBus.ClearAllSubscribersForEvent<CancelableBenchmarkEvent>();
|
var bus = CreateBenchmarkBus();
|
||||||
|
|
||||||
var dummy = 0;
|
var dummy = 0;
|
||||||
var handlers = new Action<CancelableBenchmarkEvent>[10];
|
var handlers = new Action<CancelableBenchmarkEvent>[10];
|
||||||
for (var i = 0; i < 10; i++)
|
for (var i = 0; i < handlers.Length; i++)
|
||||||
{
|
{
|
||||||
handlers[i] = e => { dummy = e.Value; };
|
handlers[i] = evt => { dummy = evt.Value; };
|
||||||
EventBus.RegisterEvent<CancelableBenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
bus.RegisterEvent<CancelableBenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
var evt = new CancelableBenchmarkEvent { Value = 1 };
|
var evt = new CancelableBenchmarkEvent { Value = 1 };
|
||||||
evt.SetCanceled(true);
|
evt.SetCanceled(true);
|
||||||
|
|
||||||
for (var i = 0; i < 500; i++) EventBus.TriggerEvent(evt);
|
Warmup(() => bus.TriggerEvent(evt), 500);
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
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();
|
sw.Stop();
|
||||||
|
|
||||||
Record("TriggerEvent × 已取消事件跳过(10 订阅者)", sw.Elapsed.TotalMilliseconds, cancelIterations);
|
Record("TriggerEvent × 已取消事件跳过(10 订阅者)", sw.Elapsed.TotalMilliseconds, cancelIterations);
|
||||||
Log("完成:已取消事件跳过");
|
Log("完成:已取消事件跳过");
|
||||||
|
|
||||||
foreach (var h in handlers)
|
foreach (var handler in handlers)
|
||||||
EventBus.UnregisterEvent<CancelableBenchmarkEvent>(h);
|
bus.UnregisterEvent<CancelableBenchmarkEvent>(handler);
|
||||||
|
|
||||||
GC.KeepAlive(dummy);
|
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<UniTask<bool>> action, int count)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Log(string msg)
|
||||||
|
{
|
||||||
Debug.Log($"[Benchmark] {msg}");
|
Debug.Log($"[Benchmark] {msg}");
|
||||||
|
}
|
||||||
|
|
||||||
private void Record(string label, double totalMs, int count)
|
private void Record(string label, double totalMs, int count)
|
||||||
{
|
{
|
||||||
var perOp = totalMs / count * 1000.0;
|
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($"\n ▶ {label}");
|
||||||
_report.AppendLine($" 总耗时 : {totalMs,10:F3} ms");
|
_report.AppendLine($" 总耗时 : {totalMs,10:F3} ms");
|
||||||
_report.AppendLine($" 单次 : {perOp,10:F4} μs/op");
|
_report.AppendLine($" 单次 : {perOp,10:F4} μs/op");
|
||||||
_report.AppendLine($" 吞吐量 : {throughput,10:F0} ops/s");
|
_report.AppendLine($" 吞吐量 : {throughput,10:F0} ops/s");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<Type>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type[] SubscriberTypes { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class EventBusGeneratedRegistry
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<Type> GetStaticSubscriberTypes()
|
||||||
|
{
|
||||||
|
var types = new List<Type>();
|
||||||
|
var seen = new HashSet<Type>();
|
||||||
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
object[] attributes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
attributes = assembly.GetCustomAttributes(typeof(EventBusStaticRegistryAttribute), false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attribute in attributes.OfType<EventBusStaticRegistryAttribute>())
|
||||||
|
{
|
||||||
|
foreach (var subscriberType in attribute.SubscriberTypes ?? Array.Empty<Type>())
|
||||||
|
{
|
||||||
|
if (subscriberType == null || !seen.Add(subscriberType))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
types.Add(subscriberType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 04809187aa0ce3b43a4df12b048a1c98
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+161
-78
@@ -1,4 +1,6 @@
|
|||||||
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
@@ -6,104 +8,185 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace ShrinkEventBus
|
namespace ShrinkEventBus
|
||||||
{
|
{
|
||||||
public static class EventBusRegHelper
|
internal static class EventBusRegHelper
|
||||||
{
|
{
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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<Type> 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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static void RegisterEventHandlers(object target) => RegisterEventHandlersInternal(target);
|
public static void RegisterTarget(ShrinkEventBusInstance bus, object target, bool requireSubscriberAttribute,
|
||||||
|
bool lenientWhenNoMethods = false)
|
||||||
private static void RegisterEventHandlersInternal(object target)
|
|
||||||
{
|
{
|
||||||
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();
|
case MethodInfo method:
|
||||||
foreach (var assembly in assemblies)
|
RegisterMethod(bus, null, method, requireSubscriberAttribute: false, isStaticRegistration: true);
|
||||||
{
|
return;
|
||||||
try
|
case Type staticType:
|
||||||
{
|
RegisterDeclaredMethods(bus, null, staticType, requireSubscriberAttribute,
|
||||||
foreach (var type in assembly.GetTypes())
|
isStaticRegistration: true, lenientWhenNoMethods);
|
||||||
{
|
return;
|
||||||
if (type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
|
default:
|
||||||
continue;
|
RegisterDeclaredMethods(bus, target, target.GetType(), requireSubscriberAttribute,
|
||||||
ScanMethodsAndRegister(null,
|
isStaticRegistration: false, lenientWhenNoMethods);
|
||||||
type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic));
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
foreach (var method in methods)
|
||||||
{
|
{
|
||||||
if (target != null && method.IsStatic) continue;
|
var hasSubscribeAttribute = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false).Length > 0;
|
||||||
var attributes = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false);
|
if (!hasSubscribeAttribute)
|
||||||
if (attributes.Length == 0) continue;
|
continue;
|
||||||
var subscribeAttr = (EventSubscribeAttribute)attributes[0];
|
|
||||||
var parameters = method.GetParameters();
|
if (method.IsStatic != isStaticRegistration)
|
||||||
if (parameters.Length != 1) continue;
|
{
|
||||||
var parameterType = parameters[0].ParameterType;
|
var expected = isStaticRegistration ? "static" : "instance";
|
||||||
if (!typeof(EventBase).IsAssignableFrom(parameterType)) continue;
|
throw new InvalidOperationException(
|
||||||
ProcessMethodRegistration(target, method, subscribeAttr, parameterType);
|
$"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)
|
EventSubscribeAttribute subscribeAttr, Type parameterType)
|
||||||
{
|
{
|
||||||
try
|
string scope = target == null ? "Static" : "Instance";
|
||||||
{
|
string typeName = target == null ? method.DeclaringType?.Name ?? "Unknown" : target.GetType().Name;
|
||||||
string scope = target == null ? "Static" : "Instance";
|
|
||||||
string typeName = target == null ? method.DeclaringType?.Name : target.GetType().Name;
|
|
||||||
|
|
||||||
if (method.ReturnType == typeof(UniTask))
|
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 UNITY_EDITOR
|
var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(UniTask));
|
||||||
Debug.LogWarning($"[EventBus] Registration failed for {method.Name}: {ex.Message}");
|
var handlerDelegate = target == null
|
||||||
#endif
|
? 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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace ShrinkEventBus
|
|
||||||
{
|
|
||||||
internal static class EventCache<TEvent> where TEvent : EventBase
|
|
||||||
{
|
|
||||||
public static ListenerList List;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 0c41a516998341adbddce082cec09938
|
|
||||||
timeCreated: 1772889485
|
|
||||||
@@ -18,8 +18,9 @@ namespace ShrinkEventBus
|
|||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Cannot clone event type {source.GetType().FullName}. A public parameterless constructor is required.");
|
$"Cannot clone event type {source.GetType().FullName}. A public parameterless constructor is required.");
|
||||||
|
|
||||||
|
// 先固化 EventId,让克隆与原事件共享同一个派发标识
|
||||||
|
_ = source.EventId;
|
||||||
CopyFields(source, clone);
|
CopyFields(source, clone);
|
||||||
CopyListenerSnapshot(source, clone);
|
|
||||||
clone.ReleaseAction = null;
|
clone.ReleaseAction = null;
|
||||||
clone.IsInPool = false;
|
clone.IsInPool = false;
|
||||||
return clone;
|
return clone;
|
||||||
@@ -32,16 +33,6 @@ namespace ShrinkEventBus
|
|||||||
fields[i].SetValue(target, fields[i].GetValue(source));
|
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)
|
private static FieldInfo[] GetCopyableFields(Type type)
|
||||||
{
|
{
|
||||||
lock (CacheLock)
|
lock (CacheLock)
|
||||||
@@ -77,8 +68,7 @@ namespace ShrinkEventBus
|
|||||||
|
|
||||||
private static bool ShouldSkipField(FieldInfo field)
|
private static bool ShouldSkipField(FieldInfo field)
|
||||||
{
|
{
|
||||||
return field.Name is "_listenerList"
|
return field.Name is "<ReleaseAction>k__BackingField"
|
||||||
or "<ReleaseAction>k__BackingField"
|
|
||||||
or "<IsInPool>k__BackingField";
|
or "<IsInPool>k__BackingField";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
#pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
|
#pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +13,10 @@ namespace ShrinkEventBus
|
|||||||
|
|
||||||
public class EventHandlerInfo
|
public class EventHandlerInfo
|
||||||
{
|
{
|
||||||
|
public long SubscriptionId { get; }
|
||||||
public Delegate Handler { get; }
|
public Delegate Handler { get; }
|
||||||
|
internal Action<EventBase>? SyncInvoker { get; }
|
||||||
|
internal Func<EventBase, UniTask>? AsyncInvoker { get; }
|
||||||
public EventPriority Priority { get; }
|
public EventPriority Priority { get; }
|
||||||
public int NumericPriority { get; }
|
public int NumericPriority { get; }
|
||||||
public bool ReceiveCanceled { get; }
|
public bool ReceiveCanceled { get; }
|
||||||
@@ -24,11 +28,17 @@ namespace ShrinkEventBus
|
|||||||
public MethodInfo? OriginalMethod { get; }
|
public MethodInfo? OriginalMethod { get; }
|
||||||
public string OriginalMethodName { get; }
|
public string OriginalMethodName { get; }
|
||||||
public Type? OriginalDeclaringType { get; }
|
public Type? OriginalDeclaringType { get; }
|
||||||
|
public DateTime RegisteredAtUtc { get; }
|
||||||
|
|
||||||
public EventHandlerInfo(Delegate handler, EventPriority priority, int numericPriority,
|
private EventHandlerInfo(long subscriptionId, Delegate handler, Action<EventBase>? syncInvoker,
|
||||||
bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = null)
|
Func<EventBase, UniTask>? asyncInvoker,
|
||||||
|
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
|
||||||
|
MethodInfo? originalMethod = null)
|
||||||
{
|
{
|
||||||
|
SubscriptionId = subscriptionId;
|
||||||
Handler = handler;
|
Handler = handler;
|
||||||
|
SyncInvoker = syncInvoker;
|
||||||
|
AsyncInvoker = asyncInvoker;
|
||||||
Priority = priority;
|
Priority = priority;
|
||||||
NumericPriority = numericPriority;
|
NumericPriority = numericPriority;
|
||||||
ReceiveCanceled = receiveCanceled;
|
ReceiveCanceled = receiveCanceled;
|
||||||
@@ -50,6 +60,8 @@ namespace ShrinkEventBus
|
|||||||
OriginalMethodName = MethodName;
|
OriginalMethodName = MethodName;
|
||||||
OriginalDeclaringType = DeclaringType;
|
OriginalDeclaringType = DeclaringType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RegisteredAtUtc = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler)
|
private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler)
|
||||||
@@ -64,5 +76,66 @@ namespace ShrinkEventBus
|
|||||||
|
|
||||||
public string DisplayMethodName => OriginalMethodName;
|
public string DisplayMethodName => OriginalMethodName;
|
||||||
public Type DisplayDeclaringType => OriginalDeclaringType ?? DeclaringType;
|
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<EventBase>? syncInvoker = null;
|
||||||
|
Func<EventBase, UniTask>? 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<EventBase> CreateSyncInvoker(Type eventType, Delegate handler)
|
||||||
|
{
|
||||||
|
var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateSyncInvokerGeneric),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType);
|
||||||
|
return (Action<EventBase>)factory.Invoke(null, new object[] { handler })!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<EventBase, UniTask> CreateAsyncInvoker(Type eventType, Delegate handler)
|
||||||
|
{
|
||||||
|
var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateAsyncInvokerGeneric),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType);
|
||||||
|
return (Func<EventBase, UniTask>)factory.Invoke(null, new object[] { handler })!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Action<EventBase> CreateSyncInvokerGeneric<TEvent>(Delegate handler) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
var typedHandler = (Action<TEvent>)handler;
|
||||||
|
return eventArgs => typedHandler((TEvent)eventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<EventBase, UniTask> CreateAsyncInvokerGeneric<TEvent>(Delegate handler)
|
||||||
|
where TEvent : EventBase
|
||||||
|
{
|
||||||
|
var typedHandler = (Func<TEvent, UniTask>)handler;
|
||||||
|
return eventArgs => typedHandler((TEvent)eventArgs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-6
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ShrinkEventBus
|
namespace ShrinkEventBus
|
||||||
{
|
{
|
||||||
public static class EventPool<T> where T : EventBase, new()
|
public static class EventPool<T> where T : EventBase, new()
|
||||||
{
|
{
|
||||||
|
private const int MaxPoolSize = 128;
|
||||||
|
|
||||||
private static readonly Stack<T> Pool = new(32);
|
private static readonly Stack<T> Pool = new(32);
|
||||||
private static readonly object Lock = new();
|
private static readonly object Lock = new();
|
||||||
private static readonly Action<EventBase> CachedReleaseAction = e => Release((T)e);
|
private static readonly Action<EventBase> CachedReleaseAction = e => Release((T)e);
|
||||||
@@ -28,15 +30,18 @@ namespace ShrinkEventBus
|
|||||||
|
|
||||||
public static void Release(T evt)
|
public static void Release(T evt)
|
||||||
{
|
{
|
||||||
if (evt == null || evt.IsInPool) return;
|
if (evt == null)
|
||||||
|
return;
|
||||||
evt.ResetInternal();
|
|
||||||
evt.IsInPool = true;
|
|
||||||
|
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
|
if (evt.IsInPool || Pool.Count >= MaxPoolSize)
|
||||||
|
return;
|
||||||
|
|
||||||
|
evt.ResetInternal();
|
||||||
|
evt.IsInPool = true;
|
||||||
Pool.Push(evt);
|
Pool.Push(evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{
|
{
|
||||||
>= 100 => EventPriority.HIGHEST,
|
>= 100 => EventPriority.HIGHEST,
|
||||||
>= 50 => EventPriority.HIGH,
|
>= 50 => EventPriority.HIGH,
|
||||||
> 0 => EventPriority.NORMAL,
|
>= 0 => EventPriority.NORMAL,
|
||||||
>= -50 => EventPriority.LOW,
|
>= -50 => EventPriority.LOW,
|
||||||
_ => EventPriority.LOWEST
|
_ => EventPriority.LOWEST
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<EventBase, Type> OnEventTriggered;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
event Action<EventBase, string, string, EventHandlerInfo[]> 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<TEvent>(Action<TEvent> handler, EventPriority priority = EventPriority.NORMAL,
|
||||||
|
bool receiveCanceled = false) where TEvent : EventBase;
|
||||||
|
void RegisterEvent<TEvent>(Action<TEvent> handler, int priority) where TEvent : EventBase;
|
||||||
|
void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority = EventPriority.NORMAL,
|
||||||
|
bool receiveCanceled = false) where TEvent : EventBase;
|
||||||
|
void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority) where TEvent : EventBase;
|
||||||
|
IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler,
|
||||||
|
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase;
|
||||||
|
IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||||
|
where TEvent : EventBase;
|
||||||
|
IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
|
||||||
|
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase;
|
||||||
|
IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||||
|
where TEvent : EventBase;
|
||||||
|
|
||||||
|
void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase;
|
||||||
|
void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase;
|
||||||
|
void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase;
|
||||||
|
void UnregisterAllEventsForObject(object targetObject);
|
||||||
|
void UnregisterAllEvents();
|
||||||
|
|
||||||
|
bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase;
|
||||||
|
bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase;
|
||||||
|
UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase;
|
||||||
|
UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase;
|
||||||
|
|
||||||
|
EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase;
|
||||||
|
ListenerList GetListenerList<TEvent>() where TEvent : EventBase;
|
||||||
|
IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot();
|
||||||
|
IReadOnlyList<ShrinkEventSubscriptionSnapshot> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cb69f308f5c727542ac602709b02c628
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+179
-46
@@ -1,50 +1,84 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace ShrinkEventBus
|
namespace ShrinkEventBus
|
||||||
{
|
{
|
||||||
public class ListenerList
|
public class ListenerList
|
||||||
{
|
{
|
||||||
private readonly object _lock = new();
|
private static readonly EventPriority[] Priorities = (EventPriority[])Enum.GetValues(typeof(EventPriority));
|
||||||
private readonly List<EventHandlerInfo> _list = new();
|
|
||||||
|
private readonly object _lock;
|
||||||
|
private readonly List<EventHandlerInfo>[] _priorityBuckets;
|
||||||
|
private readonly ListenerList? _parent;
|
||||||
|
private List<ListenerList>? _children;
|
||||||
private EventHandlerInfo[] _snapshot = Array.Empty<EventHandlerInfo>();
|
private EventHandlerInfo[] _snapshot = Array.Empty<EventHandlerInfo>();
|
||||||
|
private readonly EventHandlerInfo[]?[] _phaseSnapshots = new EventHandlerInfo[Priorities.Length][];
|
||||||
private bool _dirty;
|
private bool _dirty;
|
||||||
|
|
||||||
public void Add(Delegate handler, EventPriority priority, int numericPriority,
|
public ListenerList(ListenerList? parent = null) : this(parent, null)
|
||||||
bool receiveCanceled, string debugInfo = "", MethodInfo? originalMethod = 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<EventHandlerInfo>[Priorities.Length];
|
||||||
|
for (var i = 0; i < _priorityBuckets.Length; i++)
|
||||||
|
_priorityBuckets[i] = new List<EventHandlerInfo>();
|
||||||
|
|
||||||
|
_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)
|
public void Add(EventHandlerInfo info)
|
||||||
{
|
{
|
||||||
AddInfo(info);
|
if (info == null)
|
||||||
}
|
throw new ArgumentNullException(nameof(info));
|
||||||
|
|
||||||
private void AddInfo(EventHandlerInfo info)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var index = BinarySearchInsertIndex(info.Priority, info.NumericPriority);
|
var list = _priorityBuckets[(int)info.Priority];
|
||||||
_list.Insert(index, info);
|
var index = BinarySearchInsertIndex(list, info.NumericPriority);
|
||||||
_dirty = true;
|
list.Insert(index, info);
|
||||||
|
MarkDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Remove(Delegate handler)
|
public bool Remove(Delegate handler)
|
||||||
{
|
{
|
||||||
|
if (handler == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
lock (_lock)
|
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);
|
if (!list[i].Handler.Equals(handler))
|
||||||
_dirty = true;
|
continue;
|
||||||
|
|
||||||
|
list.RemoveAt(i);
|
||||||
|
MarkDirty();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,21 +87,55 @@ namespace ShrinkEventBus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int RemoveWhere(Predicate<EventHandlerInfo> 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)
|
public void RemoveTarget(object target)
|
||||||
{
|
{
|
||||||
|
if (target == null)
|
||||||
|
return;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var removed = false;
|
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;
|
removed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removed) _dirty = true;
|
if (removed)
|
||||||
|
MarkDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,20 +144,20 @@ namespace ShrinkEventBus
|
|||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_dirty)
|
if (_dirty)
|
||||||
{
|
RebuildSnapshot();
|
||||||
_snapshot = _list.ToArray();
|
|
||||||
_dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _snapshot;
|
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<EventHandlerInfo>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,33 +165,98 @@ namespace ShrinkEventBus
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_list.Clear();
|
foreach (var list in _priorityBuckets)
|
||||||
_snapshot = Array.Empty<EventHandlerInfo>();
|
list.Clear();
|
||||||
_dirty = false;
|
MarkDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int BinarySearchInsertIndex(EventPriority priority, int numericPriority)
|
private void AddChild(ListenerList child)
|
||||||
|
{
|
||||||
|
if (child == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_children ??= new List<ListenerList>(2);
|
||||||
|
_children.Add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkDirty()
|
||||||
|
{
|
||||||
|
_dirty = true;
|
||||||
|
_snapshot = Array.Empty<EventHandlerInfo>();
|
||||||
|
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<EventHandlerInfo>();
|
||||||
|
|
||||||
|
for (var i = 0; i < Priorities.Length; i++)
|
||||||
|
{
|
||||||
|
var parentHandlers = _parent != null
|
||||||
|
? _parent.GetHandlers(Priorities[i])
|
||||||
|
: Array.Empty<EventHandlerInfo>();
|
||||||
|
|
||||||
|
var phaseSnapshot = MergeByNumericPriority(_priorityBuckets[i], parentHandlers);
|
||||||
|
_phaseSnapshots[i] = phaseSnapshot;
|
||||||
|
merged.AddRange(phaseSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
_snapshot = merged.ToArray();
|
||||||
|
_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 两侧均已按 NumericPriority 降序排列;平局时本类型 handler 在前
|
||||||
|
private static EventHandlerInfo[] MergeByNumericPriority(List<EventHandlerInfo> 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<EventHandlerInfo> list, int numericPriority)
|
||||||
{
|
{
|
||||||
var lo = 0;
|
var lo = 0;
|
||||||
var hi = _list.Count;
|
var hi = list.Count;
|
||||||
|
|
||||||
while (lo < hi)
|
while (lo < hi)
|
||||||
{
|
{
|
||||||
var mid = (lo + hi) >> 1;
|
var mid = (lo + hi) >> 1;
|
||||||
var existing = _list[mid];
|
var cmp = numericPriority.CompareTo(list[mid].NumericPriority);
|
||||||
var cmp = existing.Priority.CompareTo(priority);
|
if (cmp > 0)
|
||||||
|
|
||||||
if (cmp == 0)
|
|
||||||
cmp = numericPriority.CompareTo(existing.NumericPriority);
|
|
||||||
|
|
||||||
if (cmp < 0)
|
|
||||||
lo = mid + 1;
|
|
||||||
else
|
|
||||||
hi = mid;
|
hi = mid;
|
||||||
|
else
|
||||||
|
lo = mid + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lo;
|
return lo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<Type>? 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<Type> checker)
|
||||||
|
{
|
||||||
|
if (checker == null)
|
||||||
|
throw new ArgumentNullException(nameof(checker));
|
||||||
|
|
||||||
|
EventClassChecker = EventClassChecker == null
|
||||||
|
? checker
|
||||||
|
: EventClassChecker + checker;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShrinkEventBusBuilder MarkerInterface<TMarker>() 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aad8b06ecb53b8a4998fd9944673e537
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<Type, ListenerList> _eventHandlers = new();
|
||||||
|
private readonly HashSet<object> _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<Type>? _eventClassChecker;
|
||||||
|
private readonly bool _checkTypesOnDispatch;
|
||||||
|
private readonly bool _allowPerPhaseDispatch;
|
||||||
|
|
||||||
|
public event Action<EventBase, Type>? OnEventTriggered;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
public bool EnableDebugRecord { get; set; }
|
||||||
|
public event Action<EventBase, string, string, EventHandlerInfo[]>? 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<TEvent>(Func<TEvent, UniTask> 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<TEvent>(Action<TEvent> 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<TEvent>(Action<TEvent> 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<TEvent>(Func<TEvent, UniTask> 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<TEvent>(Action<TEvent> 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<TEvent>(Action<TEvent> 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<TEvent>(Func<TEvent, UniTask> 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<TEvent>(Func<TEvent, UniTask> 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<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
if (handler == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var collection = TryGetListenerList(typeof(TEvent));
|
||||||
|
collection?.Remove(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
if (handler == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var collection = TryGetListenerList(typeof(TEvent));
|
||||||
|
collection?.Remove(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void ClearAllSubscribersForEvent<TEvent>() 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<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
return TriggerEventAsyncInternal(eventArgs, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
EnsurePerPhaseDispatchAllowed();
|
||||||
|
return TriggerEventAsyncInternal(eventArgs, phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
return TriggerEventInternal(eventArgs, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase
|
||||||
|
{
|
||||||
|
EnsurePerPhaseDispatchAllowed();
|
||||||
|
return TriggerEventInternal(eventArgs, phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask<bool> 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<EventHandlerInfo>();
|
||||||
|
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<UniTask> 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<TEvent>() where TEvent : EventBase
|
||||||
|
{
|
||||||
|
var listenerList = TryGetListenerList(typeof(TEvent));
|
||||||
|
var handlers = listenerList?.GetHandlers();
|
||||||
|
if (handlers == null || handlers.Length == 0)
|
||||||
|
return Array.Empty<EventHandlerInfo>();
|
||||||
|
|
||||||
|
return (EventHandlerInfo[])handlers.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenerList GetListenerList<TEvent>() where TEvent : EventBase
|
||||||
|
{
|
||||||
|
lock (_listenerLock)
|
||||||
|
{
|
||||||
|
return GetOrCreateListenerList(typeof(TEvent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot()
|
||||||
|
{
|
||||||
|
lock (_listenerLock)
|
||||||
|
{
|
||||||
|
var snapshot = new Dictionary<Type, EventHandlerInfo[]>(_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<ShrinkEventSubscriptionSnapshot> GetActiveSubscriptionsSnapshot()
|
||||||
|
{
|
||||||
|
lock (_listenerLock)
|
||||||
|
{
|
||||||
|
var snapshot = new List<ShrinkEventSubscriptionSnapshot>();
|
||||||
|
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<EventHandlerInfo> 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().");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 92c1c7cd96e37c8458600728c7c0c57f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6f28409a6ba08444cbdbfc5eb37b1809
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<PlainEvent>(_ => { }, EventPriority.NORMAL);
|
||||||
|
Assert.IsTrue(bus.TriggerEvent(new PlainEvent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CanceledEvent_SkipsHandlersUnlessReceiveCanceled()
|
||||||
|
{
|
||||||
|
var bus = EventBus.CreateBus();
|
||||||
|
var order = new List<string>();
|
||||||
|
|
||||||
|
bus.RegisterEvent<CancelableEvent>(evt =>
|
||||||
|
{
|
||||||
|
order.Add("canceler");
|
||||||
|
evt.SetCanceled(true);
|
||||||
|
}, EventPriority.HIGHEST);
|
||||||
|
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
|
||||||
|
bus.RegisterEvent<CancelableEvent>(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<UnsupportedOperationException>(() => evt.SetCanceled(true));
|
||||||
|
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetResult_OnEventWithoutResult_ThrowsWithMessage()
|
||||||
|
{
|
||||||
|
var evt = new PlainEvent();
|
||||||
|
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => evt.SetResult(EventResult.ALLOW));
|
||||||
|
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetResult_OnResultEvent_PersistsResult()
|
||||||
|
{
|
||||||
|
var bus = EventBus.CreateBus();
|
||||||
|
bus.RegisterEvent<ResultEvent>(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<PlainEvent>(_ => 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<PlainEvent>(_ => 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<string>();
|
||||||
|
|
||||||
|
bus.RegisterEvent<PlainEvent>(_ => order.Add("high"), EventPriority.HIGH);
|
||||||
|
bus.RegisterEvent<PlainEvent>(_ => 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<InvalidOperationException>(() => bus.TriggerEvent(EventPriority.HIGH, new PlainEvent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TriggerEventAsync_RunsHandlersSequentiallyByPriority()
|
||||||
|
{
|
||||||
|
var bus = EventBus.CreateBus();
|
||||||
|
var order = new List<string>();
|
||||||
|
|
||||||
|
async UniTask AsyncHandler(PlainEvent evt)
|
||||||
|
{
|
||||||
|
order.Add("async-high");
|
||||||
|
await UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.RegisterEvent<PlainEvent>(AsyncHandler, EventPriority.HIGH, receiveCanceled: false);
|
||||||
|
bus.RegisterEvent<PlainEvent>(_ => 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<string>();
|
||||||
|
|
||||||
|
async UniTask CancelingHandler(CancelableEvent evt)
|
||||||
|
{
|
||||||
|
evt.SetCanceled(true);
|
||||||
|
order.Add("async-canceler");
|
||||||
|
await UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.RegisterEvent<CancelableEvent>(CancelingHandler, EventPriority.HIGHEST, receiveCanceled: false);
|
||||||
|
bus.RegisterEvent<CancelableEvent>(_ => 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<PlainEvent>(_ => { }, 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 11b7dddad0d215b47a7249e74a574105
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<string>();
|
||||||
|
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("LOW"), EventPriority.LOW);
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("MONITOR"), EventPriority.MONITOR);
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("HIGHEST"), EventPriority.HIGHEST);
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => 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<string>();
|
||||||
|
|
||||||
|
// 5 与 10 都映射到 NORMAL 档
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("p5"), 5);
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => 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<string>();
|
||||||
|
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("first"), 0);
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => 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<BaseEvent>(_ => { }, 0);
|
||||||
|
|
||||||
|
var subscribers = bus.GetEventSubscribers<BaseEvent>();
|
||||||
|
|
||||||
|
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<BaseEvent>(Handler);
|
||||||
|
|
||||||
|
var subscribers = bus.GetEventSubscribers<BaseEvent>();
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent10"), 10);
|
||||||
|
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child5"), 5);
|
||||||
|
bus.RegisterEvent<DerivedEvent>(_ => 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<string>();
|
||||||
|
|
||||||
|
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent0"), 0);
|
||||||
|
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child0"), 0);
|
||||||
|
|
||||||
|
bus.TriggerEvent(new DerivedEvent());
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(new[] { "child0", "parent0" }, order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fc534f0e8598cab47b393ccffc90ad98
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<InvalidOperationException>(() => bus.Register(new InvalidSignatureSubscriber()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AutoRegister_TypeWithoutSubscriberAttribute_Throws()
|
||||||
|
{
|
||||||
|
var bus = EventBus.CreateBus();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => 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<ProbeEvent>(_ => { }, EventPriority.NORMAL);
|
||||||
|
|
||||||
|
var first = bus.GetEventSubscribers<ProbeEvent>();
|
||||||
|
Assert.AreEqual(1, first.Length);
|
||||||
|
|
||||||
|
first[0] = null;
|
||||||
|
var second = bus.GetEventSubscribers<ProbeEvent>();
|
||||||
|
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<PooledEvent>.Get();
|
||||||
|
evt.Value = 42;
|
||||||
|
|
||||||
|
EventPool<PooledEvent>.Release(evt);
|
||||||
|
var reused = EventPool<PooledEvent>.Get();
|
||||||
|
|
||||||
|
Assert.AreSame(evt, reused);
|
||||||
|
Assert.AreEqual(0, reused.Value);
|
||||||
|
|
||||||
|
EventPool<PooledEvent>.Release(reused);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EventPool_DoubleRelease_DoesNotDuplicatePoolEntry()
|
||||||
|
{
|
||||||
|
var evt = EventPool<PooledEvent>.Get();
|
||||||
|
|
||||||
|
EventPool<PooledEvent>.Release(evt);
|
||||||
|
EventPool<PooledEvent>.Release(evt);
|
||||||
|
|
||||||
|
var first = EventPool<PooledEvent>.Get();
|
||||||
|
var second = EventPool<PooledEvent>.Get();
|
||||||
|
|
||||||
|
Assert.AreSame(evt, first);
|
||||||
|
Assert.AreNotSame(evt, second);
|
||||||
|
|
||||||
|
EventPool<PooledEvent>.Release(first);
|
||||||
|
EventPool<PooledEvent>.Release(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EventPool_DisposeReturnsToPool()
|
||||||
|
{
|
||||||
|
PooledEvent captured;
|
||||||
|
using (var evt = EventPool<PooledEvent>.Get())
|
||||||
|
{
|
||||||
|
captured = evt;
|
||||||
|
evt.Value = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reused = EventPool<PooledEvent>.Get();
|
||||||
|
Assert.AreSame(captured, reused);
|
||||||
|
Assert.AreEqual(0, reused.Value);
|
||||||
|
|
||||||
|
EventPool<PooledEvent>.Release(reused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4570cac2813143c40b9a8edeadf19057
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<SampleEvent>(_ => firstHits++);
|
||||||
|
using var second = EventBus.SubscribeEvent<SampleEvent>(_ => 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<SampleEvent>(_ => { }, 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<SampleEvent>(Handler);
|
||||||
|
EventBus.TriggerEvent(new SampleEvent { Value = 3 });
|
||||||
|
|
||||||
|
Assert.AreEqual(3, hits);
|
||||||
|
|
||||||
|
EventBus.UnregisterEvent<SampleEvent>(Handler);
|
||||||
|
EventBus.TriggerEvent(new SampleEvent { Value = 5 });
|
||||||
|
|
||||||
|
Assert.AreEqual(3, hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ActiveSubscriptionSnapshot_EmptyAfterDispose()
|
||||||
|
{
|
||||||
|
var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => Debug.Log("noop"));
|
||||||
|
subscription.Dispose();
|
||||||
|
|
||||||
|
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, snapshots.Count);
|
||||||
|
Assert.IsTrue(subscription.IsDisposed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f5ea531619ceaeb46af4f56454624ef7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8c47d36b8919bbf4d9c39817eaf577a9
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.cneicy.shrink-eventbus",
|
"name": "com.cneicy.shrink-eventbus",
|
||||||
"version": "1.1.5",
|
"version": "1.3.0",
|
||||||
"displayName": "ShrinkEventBus",
|
"displayName": "ShrinkEventBus",
|
||||||
"description": "高性能、类型安全的 Unity 事件总线,支持优先级调度与自动注册。",
|
"description": "高性能、类型安全的 Unity 事件总线,支持优先级调度与自动注册。",
|
||||||
"unity": "2022.3",
|
"unity": "2022.3",
|
||||||
|
|||||||
Reference in new issue
Block a user