feat(packages): 内置 SDK 包并完善 ContextLoader 集成
- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档 - 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理 - 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言 - 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则 - 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护 - 新增 Network 语义扫描、模板保护和 App 组件生命周期测试 - 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试 - 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43e85f78c24f1d94ba7d06a4fa37e937
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class CancelableAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class HasResultAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class EventBusSubscriberAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public class EventSubscribeAttribute : Attribute
|
||||
{
|
||||
public EventPriority Priority { get; set; }
|
||||
public bool ReceiveCanceled { get; set; }
|
||||
public int NumericPriority { get; set; }
|
||||
|
||||
public EventSubscribeAttribute()
|
||||
{
|
||||
Priority = EventPriority.NORMAL;
|
||||
ReceiveCanceled = false;
|
||||
NumericPriority = 0;
|
||||
}
|
||||
|
||||
public EventSubscribeAttribute(EventPriority priority, bool receiveCanceled = false)
|
||||
{
|
||||
Priority = priority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
NumericPriority = 0;
|
||||
}
|
||||
|
||||
public EventSubscribeAttribute(int priority)
|
||||
{
|
||||
NumericPriority = priority;
|
||||
Priority = PriorityHelper.ConvertToEventPriority(priority);
|
||||
ReceiveCanceled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bb9d82b04a04f3085b9e68ab4f1ba60
|
||||
timeCreated: 1760098802
|
||||
@@ -0,0 +1,36 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public static class EventAutoRegHelper
|
||||
{
|
||||
private static readonly object InitLock = new();
|
||||
public static bool IsInitialized { get; private set; }
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void InitializeAfterSceneLoad()
|
||||
{
|
||||
EnsureInitialized();
|
||||
}
|
||||
|
||||
public static void EnsureInitialized()
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
lock (InitLock)
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
IsInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
lock (InitLock)
|
||||
{
|
||||
IsInitialized = false;
|
||||
}
|
||||
|
||||
EventBus.UnregisterAllEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d79f65274145cab7614950a64a92f5
|
||||
timeCreated: 1760099405
|
||||
@@ -0,0 +1,146 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public abstract class EventBase : IDisposable
|
||||
{
|
||||
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 EventResult _result = EventResult.DEFAULT;
|
||||
|
||||
[IgnoreDataMember]
|
||||
public EventHandlerInfo? CurrentHandler { get; internal set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public DateTime EventTime { get; private set; } = DateTime.UtcNow;
|
||||
|
||||
[IgnoreDataMember]
|
||||
public Guid EventId => _eventId ??= Guid.NewGuid();
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool IsCancelable { get; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool HasResult { get; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public EventPriority? Phase { get; private set; }
|
||||
|
||||
internal bool IsInPool { get; set; }
|
||||
internal Action<EventBase>? ReleaseAction { get; set; }
|
||||
|
||||
protected EventBase()
|
||||
{
|
||||
var metadata = GetOrCreateMetadata(GetType());
|
||||
IsCancelable = metadata.IsCancelable;
|
||||
HasResult = metadata.HasResult;
|
||||
Setup();
|
||||
}
|
||||
|
||||
protected virtual void Setup() { }
|
||||
|
||||
protected virtual void OnReset() { }
|
||||
|
||||
internal void ResetInternal()
|
||||
{
|
||||
_isCanceled = false;
|
||||
_result = EventResult.DEFAULT;
|
||||
CurrentHandler = null;
|
||||
Phase = null;
|
||||
EventTime = DateTime.UtcNow;
|
||||
_eventId = null;
|
||||
_dispatchSnapshot = null;
|
||||
OnReset();
|
||||
}
|
||||
|
||||
internal void PrepareForDispatch()
|
||||
{
|
||||
CurrentHandler = null;
|
||||
Phase = null;
|
||||
EventTime = DateTime.UtcNow;
|
||||
_eventId = null;
|
||||
_dispatchSnapshot = null;
|
||||
}
|
||||
|
||||
internal void SetListenerSnapshot(EventHandlerInfo[]? handlers)
|
||||
{
|
||||
_dispatchSnapshot = handlers is { Length: > 0 } ? handlers : null;
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool IsCanceled
|
||||
{
|
||||
get => _isCanceled;
|
||||
set
|
||||
{
|
||||
if (!IsCancelable)
|
||||
throw new UnsupportedOperationException(
|
||||
$"Event {GetType().Name} is not cancelable. Mark it with [Cancelable] to allow cancellation.");
|
||||
_isCanceled = value;
|
||||
}
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public EventResult Result
|
||||
{
|
||||
get => _result;
|
||||
set
|
||||
{
|
||||
if (!HasResult)
|
||||
throw new InvalidOperationException(
|
||||
$"Event {GetType().Name} does not support results. Mark it with [HasResult] to allow setting a result.");
|
||||
_result = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetPhase(EventPriority value)
|
||||
{
|
||||
if (Phase == value) return;
|
||||
if (Phase != null && Phase.Value.CompareTo(value) > 0)
|
||||
throw new ArgumentException(
|
||||
$"Event phase cannot move backwards from {Phase.Value} to {value}.", nameof(value));
|
||||
Phase = value;
|
||||
}
|
||||
|
||||
public void SetCanceled(bool canceled) => IsCanceled = canceled;
|
||||
public void SetResult(EventResult result) => Result = result;
|
||||
|
||||
public 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()
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b978c5174cf44d893a41950e7a504e3
|
||||
timeCreated: 1760098714
|
||||
@@ -0,0 +1,129 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public static class EventBus
|
||||
{
|
||||
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
|
||||
public static bool EnableDebugRecord
|
||||
{
|
||||
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
|
||||
|
||||
public static ShrinkEventBusBuilder Builder() => new();
|
||||
|
||||
public static IShrinkEventBus CreateBus(Action<ShrinkEventBusBuilder>? configure = null)
|
||||
{
|
||||
var builder = new ShrinkEventBusBuilder();
|
||||
configure?.Invoke(builder);
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public static void Start() => DefaultBus.Start();
|
||||
public static void AutoRegister(object target) => DefaultBus.AutoRegister(target);
|
||||
public static void Register(object target) => DefaultBus.Register(target);
|
||||
public static void Unregister(object target) => DefaultBus.Unregister(target);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Action<TEvent> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler,
|
||||
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()
|
||||
{
|
||||
var bus = (ShrinkEventBusInstance)new ShrinkEventBusBuilder()
|
||||
.SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndContinue)
|
||||
.AllowPerPhaseDispatch()
|
||||
.Build();
|
||||
EventBusRegHelper.RegStaticEventHandler(bus);
|
||||
return bus;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b995a65fd1324255a5ade1ae1f0adf32
|
||||
timeCreated: 1760099076
|
||||
@@ -0,0 +1,519 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace ShrinkEventBus.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// ShrinkEventBus 性能测试。
|
||||
/// 挂在任意 GameObject 上,运行后在 Console 查看结果。
|
||||
/// 在进行测试时不要打开事件查看器的大量实时追踪。
|
||||
/// </summary>
|
||||
public sealed class EventBusBenchmark : MonoBehaviour
|
||||
{
|
||||
[Header("同步触发测试")] public int iterations = 1_000_000;
|
||||
|
||||
[Header("大批量订阅者测试")] public int subscriberCount = 30;
|
||||
public int massIterations = 10_000;
|
||||
|
||||
[Header("异步触发测试")] public int asyncIterations = 1_000;
|
||||
|
||||
[Header("注册 / 注销测试")] public int registerIterations = 5_000;
|
||||
public int scannedRegisterIterations = 1_000;
|
||||
|
||||
[Header("取消事件测试")] public int cancelIterations = 100_000;
|
||||
|
||||
public class BenchmarkEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DerivedBenchmarkEvent : BenchmarkEvent
|
||||
{
|
||||
public bool IsDerived { get; set; }
|
||||
}
|
||||
|
||||
[Cancelable]
|
||||
public class CancelableBenchmarkEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BenchmarkInstanceSubscriber
|
||||
{
|
||||
public int Recorded;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnBenchmark(BenchmarkEvent evt)
|
||||
{
|
||||
Recorded = evt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class BenchmarkStaticSubscriber
|
||||
{
|
||||
public static int Recorded;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private static void OnBenchmark(BenchmarkEvent evt)
|
||||
{
|
||||
Recorded = evt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class BenchmarkMethodSubscriber
|
||||
{
|
||||
public static int Recorded;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private static void OnBenchmark(BenchmarkEvent evt)
|
||||
{
|
||||
Recorded = evt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PhaseSubscriber
|
||||
{
|
||||
public int Sum;
|
||||
|
||||
[EventSubscribe(EventPriority.HIGH)]
|
||||
private void OnHigh(BenchmarkEvent evt)
|
||||
{
|
||||
Sum += evt.Value + 1;
|
||||
}
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnNormal(BenchmarkEvent evt)
|
||||
{
|
||||
Sum += evt.Value + 10;
|
||||
}
|
||||
|
||||
[EventSubscribe(EventPriority.LOW)]
|
||||
private void OnLow(BenchmarkEvent evt)
|
||||
{
|
||||
Sum += evt.Value + 100;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly StringBuilder _report = new();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
RunAllBenchmarks().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid RunAllBenchmarks()
|
||||
{
|
||||
_report.Clear();
|
||||
_report.AppendLine("╔══════════════════════════════════════════════════╗");
|
||||
_report.AppendLine("║ ShrinkEventBus Benchmark Report ║");
|
||||
_report.AppendLine(
|
||||
$"║ iter={iterations,8} mass={massIterations,6} reg={registerIterations,5} scan={scannedRegisterIterations,4} ║");
|
||||
_report.AppendLine("╚══════════════════════════════════════════════════╝");
|
||||
_report.AppendLine(" 基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启)");
|
||||
|
||||
Log("开始:无订阅者触发");
|
||||
Bench_TriggerEvent_NoSubscriber();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:单订阅者同步触发");
|
||||
Bench_TriggerEvent_SingleSubscriber();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:父事件监听子事件");
|
||||
Bench_TriggerEvent_InheritedSubscriber();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log($"开始:{subscriberCount} 订阅者同步触发");
|
||||
Bench_TriggerEvent_MassSubscribers();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:按 phase 分发");
|
||||
Bench_TriggerEvent_PerPhase();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:异步触发");
|
||||
await Bench_TriggerEventAsync();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:手工 delegate 注册/注销");
|
||||
Bench_RegisterUnregister_ManualDelegate();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:实例扫描注册/注销");
|
||||
Bench_RegisterUnregister_InstanceScan();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:静态类型扫描注册/注销");
|
||||
Bench_RegisterUnregister_StaticScan();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:MethodInfo 注册/注销");
|
||||
Bench_RegisterUnregister_MethodScan();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:EventPool vs new");
|
||||
Bench_EventPool_vs_New();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:已取消事件跳过");
|
||||
Bench_CanceledEvent_Skip();
|
||||
|
||||
_report.AppendLine("\n══════════════════════════════════════════════════");
|
||||
_report.AppendLine(" 全部测试完成");
|
||||
Debug.Log(_report.ToString());
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_NoSubscriber()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
|
||||
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("完成:无订阅者触发");
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_SingleSubscriber()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
|
||||
void Handler(BenchmarkEvent evt)
|
||||
{
|
||||
dummy = evt.Value;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
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_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()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
var handlers = new Action<BenchmarkEvent>[subscriberCount];
|
||||
for (var i = 0; i < subscriberCount; i++)
|
||||
{
|
||||
var captured = i;
|
||||
handlers[i] = evt => { dummy = captured + evt.Value; };
|
||||
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
Warmup(() => bus.TriggerEvent(evt), 100);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < massIterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record($"TriggerEvent × {subscriberCount} 订阅者同步", sw.Elapsed.TotalMilliseconds, massIterations);
|
||||
Log($"完成:{subscriberCount} 订阅者同步触发");
|
||||
|
||||
foreach (var handler in handlers)
|
||||
bus.UnregisterEvent<BenchmarkEvent>(handler);
|
||||
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_PerPhase()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var subscriber = new PhaseSubscriber();
|
||||
bus.Register(subscriber);
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
Warmup(() => bus.TriggerEvent(EventPriority.HIGH, evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
bus.TriggerEvent(EventPriority.HIGH, evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent(Phase=HIGH) × 3 phase 监听", sw.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:按 phase 分发");
|
||||
|
||||
bus.Unregister(subscriber);
|
||||
GC.KeepAlive(subscriber.Sum);
|
||||
}
|
||||
|
||||
private async UniTask Bench_TriggerEventAsync()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
|
||||
async UniTask Handler(BenchmarkEvent evt)
|
||||
{
|
||||
dummy = evt.Value;
|
||||
await UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
await WarmupAsync(() => bus.TriggerEventAsync(evt), 10);
|
||||
|
||||
const int perFrame = 10;
|
||||
var totalMs = 0.0;
|
||||
var ran = 0;
|
||||
|
||||
while (ran < asyncIterations)
|
||||
{
|
||||
await UniTask.Yield();
|
||||
var count = Math.Min(perFrame, asyncIterations - ran);
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < count; i++)
|
||||
await bus.TriggerEventAsync(evt);
|
||||
sw.Stop();
|
||||
totalMs += sw.Elapsed.TotalMilliseconds;
|
||||
ran += count;
|
||||
}
|
||||
|
||||
Record("TriggerEventAsync × 单订阅者异步", totalMs, asyncIterations);
|
||||
Log("完成:异步触发");
|
||||
|
||||
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_ManualDelegate()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
var handlers = new Action<BenchmarkEvent>[registerIterations];
|
||||
for (var i = 0; i < registerIterations; i++)
|
||||
{
|
||||
var captured = i;
|
||||
handlers[i] = evt => { dummy = captured; };
|
||||
}
|
||||
|
||||
var swReg = Stopwatch.StartNew();
|
||||
for (var i = 0; i < registerIterations; i++)
|
||||
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||
swReg.Stop();
|
||||
Record("RegisterEvent(delegate)", swReg.Elapsed.TotalMilliseconds, registerIterations);
|
||||
|
||||
var swUnreg = Stopwatch.StartNew();
|
||||
for (var i = 0; i < registerIterations; i++)
|
||||
bus.UnregisterEvent<BenchmarkEvent>(handlers[i]);
|
||||
swUnreg.Stop();
|
||||
Record("UnregisterEvent(delegate)", swUnreg.Elapsed.TotalMilliseconds, registerIterations);
|
||||
Log("完成:手工 delegate 注册/注销");
|
||||
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_InstanceScan()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var subscribers = new BenchmarkInstanceSubscriber[scannedRegisterIterations];
|
||||
for (var i = 0; i < subscribers.Length; i++)
|
||||
subscribers[i] = new BenchmarkInstanceSubscriber();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < subscribers.Length; i++)
|
||||
{
|
||||
var subscriber = subscribers[i];
|
||||
bus.Register(subscriber);
|
||||
bus.Unregister(subscriber);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Record("Register/Unregister(instance scan)", sw.Elapsed.TotalMilliseconds, subscribers.Length);
|
||||
Log("完成:实例扫描注册/注销");
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_StaticScan()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < scannedRegisterIterations; i++)
|
||||
{
|
||||
bus.Register(typeof(BenchmarkStaticSubscriber));
|
||||
bus.Unregister(typeof(BenchmarkStaticSubscriber));
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Record("Register/Unregister(static type scan)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations);
|
||||
Log("完成:静态类型扫描注册/注销");
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_MethodScan()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var method = typeof(BenchmarkMethodSubscriber).GetMethod("OnBenchmark",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
if (method == null)
|
||||
throw new MissingMethodException(typeof(BenchmarkMethodSubscriber).FullName, "OnBenchmark");
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < scannedRegisterIterations; i++)
|
||||
{
|
||||
bus.Register(method);
|
||||
bus.Unregister(method);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Record("Register/Unregister(MethodInfo)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations);
|
||||
Log("完成:MethodInfo 注册/注销");
|
||||
}
|
||||
|
||||
private void Bench_EventPool_vs_New()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
|
||||
void Handler(BenchmarkEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
for (var i = 0; i < 64; i++)
|
||||
EventPool<BenchmarkEvent>.Release(EventPool<BenchmarkEvent>.Get());
|
||||
|
||||
GC.Collect();
|
||||
var gcBefore = GC.CollectionCount(0);
|
||||
var swNew = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var evt = new BenchmarkEvent { Value = i };
|
||||
bus.TriggerEvent(evt);
|
||||
}
|
||||
|
||||
swNew.Stop();
|
||||
var gcNew = GC.CollectionCount(0) - gcBefore;
|
||||
Record($"TriggerEvent × new() [GC Gen0={gcNew,3}]", swNew.Elapsed.TotalMilliseconds, iterations);
|
||||
|
||||
GC.Collect();
|
||||
gcBefore = GC.CollectionCount(0);
|
||||
var swPool = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
using var evt = EventPool<BenchmarkEvent>.Get();
|
||||
evt.Value = i;
|
||||
bus.TriggerEvent(evt);
|
||||
}
|
||||
|
||||
swPool.Stop();
|
||||
var gcPool = GC.CollectionCount(0) - gcBefore;
|
||||
Record($"TriggerEvent × Pool.Get() [GC Gen0={gcPool,3}]", swPool.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:EventPool vs new");
|
||||
|
||||
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||
}
|
||||
|
||||
private void Bench_CanceledEvent_Skip()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
var handlers = new Action<CancelableBenchmarkEvent>[10];
|
||||
for (var i = 0; i < handlers.Length; i++)
|
||||
{
|
||||
handlers[i] = evt => { dummy = evt.Value; };
|
||||
bus.RegisterEvent<CancelableBenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
var evt = new CancelableBenchmarkEvent { Value = 1 };
|
||||
evt.SetCanceled(true);
|
||||
|
||||
Warmup(() => bus.TriggerEvent(evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < cancelIterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent × 已取消事件跳过(10 订阅者)", sw.Elapsed.TotalMilliseconds, cancelIterations);
|
||||
Log("完成:已取消事件跳过");
|
||||
|
||||
foreach (var handler in handlers)
|
||||
bus.UnregisterEvent<CancelableBenchmarkEvent>(handler);
|
||||
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private static IShrinkEventBus CreateBenchmarkBus()
|
||||
{
|
||||
return EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch());
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
private void Record(string label, double totalMs, int count)
|
||||
{
|
||||
var perOp = totalMs / count * 1000.0;
|
||||
var throughput = totalMs <= 0.0001 ? 0 : count / (totalMs / 1000.0);
|
||||
_report.AppendLine($"\n ▶ {label}");
|
||||
_report.AppendLine($" 总耗时 : {totalMs,10:F3} ms");
|
||||
_report.AppendLine($" 单次 : {perOp,10:F4} μs/op");
|
||||
_report.AppendLine($" 吞吐量 : {throughput,10:F0} ops/s");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9619a4dc431b4b2c8268c32a9e56f72f
|
||||
timeCreated: 1772913062
|
||||
@@ -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:
|
||||
@@ -0,0 +1,193 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal static class EventBusRegHelper
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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)
|
||||
{
|
||||
_ = ex;
|
||||
#if UNITY_EDITOR
|
||||
Debug.LogWarning($"[EventBus] Static registration failed for {type?.FullName}: {ex.Message}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void RegisterTarget(ShrinkEventBusInstance bus, object target, bool requireSubscriberAttribute,
|
||||
bool lenientWhenNoMethods = false)
|
||||
{
|
||||
if (bus == null)
|
||||
throw new ArgumentNullException(nameof(bus));
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
switch (target)
|
||||
{
|
||||
case MethodInfo method:
|
||||
RegisterMethod(bus, null, method, requireSubscriberAttribute: false, isStaticRegistration: true);
|
||||
return;
|
||||
case Type staticType:
|
||||
RegisterDeclaredMethods(bus, null, staticType, requireSubscriberAttribute,
|
||||
isStaticRegistration: true, lenientWhenNoMethods);
|
||||
return;
|
||||
default:
|
||||
RegisterDeclaredMethods(bus, target, target.GetType(), requireSubscriberAttribute,
|
||||
isStaticRegistration: false, lenientWhenNoMethods);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void 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)
|
||||
{
|
||||
var hasSubscribeAttribute = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false).Length > 0;
|
||||
if (!hasSubscribeAttribute)
|
||||
continue;
|
||||
|
||||
if (method.IsStatic != isStaticRegistration)
|
||||
{
|
||||
var expected = isStaticRegistration ? "static" : "instance";
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode.");
|
||||
}
|
||||
|
||||
RegisterMethod(bus, target, method, requireSubscriberAttribute: false, isStaticRegistration);
|
||||
foundMethods++;
|
||||
}
|
||||
|
||||
if (foundMethods == 0)
|
||||
{
|
||||
var message =
|
||||
$"Type {type.FullName} has no [EventSubscribe] methods for {(isStaticRegistration ? "static" : "instance")} registration.";
|
||||
if (lenientWhenNoMethods)
|
||||
{
|
||||
Debug.LogWarning($"[EventBus] {message} Auto-registration skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void 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)
|
||||
{
|
||||
string scope = target == null ? "Static" : "Instance";
|
||||
string typeName = target == null ? method.DeclaringType?.Name ?? "Unknown" : target.GetType().Name;
|
||||
|
||||
if (method.ReturnType == typeof(UniTask))
|
||||
{
|
||||
var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(UniTask));
|
||||
var handlerDelegate = target == null
|
||||
? Delegate.CreateDelegate(funcType, method)
|
||||
: Delegate.CreateDelegate(funcType, target, method);
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a237a407a3a42394eba27bc8e4dbdce2
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal static class EventCloneUtility
|
||||
{
|
||||
private static readonly object CacheLock = new();
|
||||
private static readonly Dictionary<Type, FieldInfo[]> FieldCache = new();
|
||||
|
||||
public static TEvent CloneForDetachedDispatch<TEvent>(TEvent source) where TEvent : EventBase
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
if (Activator.CreateInstance(source.GetType()) is not TEvent clone)
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot clone event type {source.GetType().FullName}. A public parameterless constructor is required.");
|
||||
|
||||
// 先固化 EventId,让克隆与原事件共享同一个派发标识
|
||||
_ = source.EventId;
|
||||
CopyFields(source, clone);
|
||||
clone.ReleaseAction = null;
|
||||
clone.IsInPool = false;
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static void CopyFields(EventBase source, EventBase target)
|
||||
{
|
||||
var fields = GetCopyableFields(source.GetType());
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
fields[i].SetValue(target, fields[i].GetValue(source));
|
||||
}
|
||||
|
||||
private static FieldInfo[] GetCopyableFields(Type type)
|
||||
{
|
||||
lock (CacheLock)
|
||||
{
|
||||
if (FieldCache.TryGetValue(type, out var cached))
|
||||
return cached;
|
||||
|
||||
var fields = new List<FieldInfo>();
|
||||
var currentType = type;
|
||||
while (currentType != null && currentType != typeof(object))
|
||||
{
|
||||
var declaredFields = currentType.GetFields(BindingFlags.Instance | BindingFlags.Public |
|
||||
BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
||||
for (var i = 0; i < declaredFields.Length; i++)
|
||||
{
|
||||
var field = declaredFields[i];
|
||||
if (field.IsStatic)
|
||||
continue;
|
||||
if (ShouldSkipField(field))
|
||||
continue;
|
||||
|
||||
fields.Add(field);
|
||||
}
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
cached = fields.ToArray();
|
||||
FieldCache[type] = cached;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldSkipField(FieldInfo field)
|
||||
{
|
||||
return field.Name is "<ReleaseAction>k__BackingField"
|
||||
or "<IsInPool>k__BackingField";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 876564c111495b2489870b29ab75da5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Cysharp.Threading.Tasks;
|
||||
#pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
|
||||
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public interface IMethodWrapper
|
||||
{
|
||||
MethodInfo OriginalMethod { get; }
|
||||
}
|
||||
|
||||
public class EventHandlerInfo
|
||||
{
|
||||
public long SubscriptionId { get; }
|
||||
public Delegate Handler { get; }
|
||||
internal Action<EventBase>? SyncInvoker { get; }
|
||||
internal Func<EventBase, UniTask>? AsyncInvoker { get; }
|
||||
public EventPriority Priority { get; }
|
||||
public int NumericPriority { get; }
|
||||
public bool ReceiveCanceled { get; }
|
||||
public object? Target { get; }
|
||||
public MethodInfo Method { get; }
|
||||
public string DebugInfo { get; }
|
||||
public Type DeclaringType { get; }
|
||||
public string MethodName { get; }
|
||||
public MethodInfo? OriginalMethod { get; }
|
||||
public string OriginalMethodName { get; }
|
||||
public Type? OriginalDeclaringType { get; }
|
||||
public DateTime RegisteredAtUtc { get; }
|
||||
|
||||
private EventHandlerInfo(long subscriptionId, Delegate handler, Action<EventBase>? syncInvoker,
|
||||
Func<EventBase, UniTask>? asyncInvoker,
|
||||
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
|
||||
MethodInfo? originalMethod = null)
|
||||
{
|
||||
SubscriptionId = subscriptionId;
|
||||
Handler = handler;
|
||||
SyncInvoker = syncInvoker;
|
||||
AsyncInvoker = asyncInvoker;
|
||||
Priority = priority;
|
||||
NumericPriority = numericPriority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
Target = handler.Target;
|
||||
Method = handler.Method;
|
||||
DebugInfo = debugInfo;
|
||||
DeclaringType = Method.DeclaringType ?? typeof(object);
|
||||
MethodName = Method.Name;
|
||||
|
||||
OriginalMethod = ExtractOriginalMethodFromWrapper(handler) ?? originalMethod;
|
||||
|
||||
if (OriginalMethod != null)
|
||||
{
|
||||
OriginalMethodName = OriginalMethod.Name;
|
||||
OriginalDeclaringType = OriginalMethod.DeclaringType;
|
||||
}
|
||||
else
|
||||
{
|
||||
OriginalMethodName = MethodName;
|
||||
OriginalDeclaringType = DeclaringType;
|
||||
}
|
||||
|
||||
RegisteredAtUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler)
|
||||
{
|
||||
if (handler.Target is IMethodWrapper wrapper)
|
||||
{
|
||||
return wrapper.OriginalMethod;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string DisplayMethodName => OriginalMethodName;
|
||||
public Type DisplayDeclaringType => OriginalDeclaringType ?? DeclaringType;
|
||||
|
||||
public bool MatchesMethod(MethodInfo method)
|
||||
{
|
||||
return method != null && (ReferenceEquals(Method, method) || ReferenceEquals(OriginalMethod, method));
|
||||
}
|
||||
|
||||
public bool MatchesDeclaringType(Type declaringType)
|
||||
{
|
||||
return declaringType != null &&
|
||||
(DeclaringType == declaringType || OriginalDeclaringType == declaringType);
|
||||
}
|
||||
|
||||
public static EventHandlerInfo Create(long subscriptionId, Delegate handler, Type eventType,
|
||||
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
|
||||
MethodInfo? originalMethod = null)
|
||||
{
|
||||
if (handler == null)
|
||||
throw new System.ArgumentNullException(nameof(handler));
|
||||
if (eventType == null)
|
||||
throw new System.ArgumentNullException(nameof(eventType));
|
||||
|
||||
Action<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19972a32cbf54c7f94d6214fde5635c2
|
||||
timeCreated: 1760098840
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
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 object Lock = new();
|
||||
private static readonly Action<EventBase> CachedReleaseAction = e => Release((T)e);
|
||||
|
||||
public static T Get()
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
if (Pool.Count > 0)
|
||||
{
|
||||
var evt = Pool.Pop();
|
||||
evt.IsInPool = false;
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
|
||||
var newEvt = new T();
|
||||
newEvt.ReleaseAction = CachedReleaseAction;
|
||||
return newEvt;
|
||||
}
|
||||
|
||||
public static void Release(T evt)
|
||||
{
|
||||
if (evt == null)
|
||||
return;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
if (evt.IsInPool || Pool.Count >= MaxPoolSize)
|
||||
return;
|
||||
|
||||
evt.ResetInternal();
|
||||
evt.IsInPool = true;
|
||||
Pool.Push(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c59d817792dd40f08ffed2f1a8d65cb8
|
||||
timeCreated: 1772891540
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public enum EventPriority
|
||||
{
|
||||
HIGHEST = 0,
|
||||
HIGH = 1,
|
||||
NORMAL = 2,
|
||||
LOW = 3,
|
||||
LOWEST = 4,
|
||||
MONITOR = 5
|
||||
}
|
||||
|
||||
internal static class PriorityHelper
|
||||
{
|
||||
public static EventPriority ConvertToEventPriority(int numericPriority)
|
||||
{
|
||||
return numericPriority switch
|
||||
{
|
||||
>= 100 => EventPriority.HIGHEST,
|
||||
>= 50 => EventPriority.HIGH,
|
||||
>= 0 => EventPriority.NORMAL,
|
||||
>= -50 => EventPriority.LOW,
|
||||
_ => EventPriority.LOWEST
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c77c5178c66948f29a7c87c9ae203004
|
||||
timeCreated: 1760098774
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public enum EventResult
|
||||
{
|
||||
DEFAULT,
|
||||
ALLOW,
|
||||
DENY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5213a68ce8f433a98f272904a5d7bd2
|
||||
timeCreated: 1760098791
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public class UnsupportedOperationException : InvalidOperationException
|
||||
{
|
||||
public UnsupportedOperationException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public UnsupportedOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public UnsupportedOperationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 893fdc450e7a4a8e8e1b675f0ce084a7
|
||||
timeCreated: 1760098825
|
||||
@@ -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:
|
||||
@@ -0,0 +1,262 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public class ListenerList
|
||||
{
|
||||
private static readonly EventPriority[] Priorities = (EventPriority[])Enum.GetValues(typeof(EventPriority));
|
||||
|
||||
private readonly object _lock;
|
||||
private readonly List<EventHandlerInfo>[] _priorityBuckets;
|
||||
private readonly ListenerList? _parent;
|
||||
private List<ListenerList>? _children;
|
||||
private EventHandlerInfo[] _snapshot = Array.Empty<EventHandlerInfo>();
|
||||
private readonly EventHandlerInfo[]?[] _phaseSnapshots = new EventHandlerInfo[Priorities.Length][];
|
||||
private bool _dirty;
|
||||
|
||||
public ListenerList(ListenerList? parent = null) : this(parent, null)
|
||||
{
|
||||
}
|
||||
|
||||
// 同一条父子链必须共用一把锁,否则脏标记传播与快照重建会产生竞态
|
||||
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)
|
||||
{
|
||||
if (info == null)
|
||||
throw new ArgumentNullException(nameof(info));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var list = _priorityBuckets[(int)info.Priority];
|
||||
var index = BinarySearchInsertIndex(list, info.NumericPriority);
|
||||
list.Insert(index, info);
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(Delegate handler)
|
||||
{
|
||||
if (handler == null)
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var list in _priorityBuckets)
|
||||
{
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (!list[i].Handler.Equals(handler))
|
||||
continue;
|
||||
|
||||
list.RemoveAt(i);
|
||||
MarkDirty();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var removed = false;
|
||||
foreach (var list in _priorityBuckets)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!ReferenceEquals(list[i].Target, target))
|
||||
continue;
|
||||
|
||||
list.RemoveAt(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed)
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandlerInfo[] GetHandlers()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_dirty)
|
||||
RebuildSnapshot();
|
||||
|
||||
return _snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandlerInfo[] GetHandlers(EventPriority priority)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_dirty)
|
||||
RebuildSnapshot();
|
||||
|
||||
return _phaseSnapshots[(int)priority] ?? Array.Empty<EventHandlerInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var list in _priorityBuckets)
|
||||
list.Clear();
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
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 hi = list.Count;
|
||||
|
||||
while (lo < hi)
|
||||
{
|
||||
var mid = (lo + hi) >> 1;
|
||||
var cmp = numericPriority.CompareTo(list[mid].NumericPriority);
|
||||
if (cmp > 0)
|
||||
hi = mid;
|
||||
else
|
||||
lo = mid + 1;
|
||||
}
|
||||
|
||||
return lo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 864eb3e7bf8f443c86472995e408c580
|
||||
timeCreated: 1760098877
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "ShrinkEventBus.Runtime",
|
||||
"rootNamespace": "ShrinkEventBus",
|
||||
"references": [
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9a7e109b2ecd4946ba9202d2288af15
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,718 @@
|
||||
#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;
|
||||
|
||||
// Keep the registration -> listener lock order used by RegisterCore.
|
||||
// Removing only handlers is not enough: the target must be allowed to
|
||||
// register again after a component-style teardown and reinitialization.
|
||||
lock (_registrationLock)
|
||||
{
|
||||
_registeredTargets.Remove(targetObject);
|
||||
|
||||
lock (_listenerLock)
|
||||
{
|
||||
foreach (var kvp in _eventHandlers)
|
||||
kvp.Value.RemoveTarget(targetObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UnregisterAllEvents()
|
||||
{
|
||||
// Match RegisterCore/UnregisterAllEventsForObject so teardown cannot
|
||||
// deadlock with a concurrent registration.
|
||||
lock (_registrationLock)
|
||||
{
|
||||
lock (_listenerLock)
|
||||
{
|
||||
foreach (var kvp in _eventHandlers)
|
||||
kvp.Value.Clear();
|
||||
}
|
||||
|
||||
_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:
|
||||
Reference in New Issue
Block a user