From 2cea8a7ff59bc36280a1c23d76d241f3d72e30b2 Mon Sep 17 00:00:00 2001 From: cneicy Date: Tue, 7 Apr 2026 18:13:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=8B=E4=BB=B6=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 + CHANGELOG.md.meta | 7 + Editor/EventBusViewerWindow.cs | 439 ++++++++++++++++++++++++------ README.md | 17 +- Runtime/AssemblyInfo.cs | 3 + Runtime/AssemblyInfo.cs.meta | 11 + Runtime/EventBase.cs | 26 +- Runtime/EventBus.cs | 44 +-- Runtime/EventCloneUtility.cs | 85 ++++++ Runtime/EventCloneUtility.cs.meta | 11 + package.json | 4 +- 11 files changed, 546 insertions(+), 116 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 Runtime/AssemblyInfo.cs create mode 100644 Runtime/AssemblyInfo.cs.meta create mode 100644 Runtime/EventCloneUtility.cs create mode 100644 Runtime/EventCloneUtility.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..30e2a1b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。 + +## [1.1.5] - 2026-04-07 + +### Changed + +- 修正 `PrepareForDispatch()`,现在只重置派发期状态,不再误清业务载荷。 +- 修正同步 `TriggerEvent(...)` 遇到异步 handler 时的行为,改为向异步处理器分发脱离原对象的事件快照,避免直接复用原事件对象。 +- `OnEventTriggered` 与编辑器追踪钩子改为在派发完成后触发,同时覆盖无监听者事件。 +- `ShrinkNetworkEventBusBridge` 的异步转发改为先克隆事件,降低桥接层状态污染风险。 +- 事件查看器的实时日志改为快照模型,支持关键词过滤、按“有监听者 / 无监听者”筛选、同类事件折叠聚合显示。 +- 编辑器菜单迁移到 `ShrinkSDK/事件总线/事件查看器`。 +- README 同步更新新的调试入口与同步/异步派发语义说明。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..1467115 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9b2928da4a2426743bd762f5ebb9c232 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/EventBusViewerWindow.cs b/Editor/EventBusViewerWindow.cs index ba419fe..be94529 100644 --- a/Editor/EventBusViewerWindow.cs +++ b/Editor/EventBusViewerWindow.cs @@ -12,16 +12,61 @@ namespace ShrinkEventBus.Editor { public class EventBusViewerWindow : EditorWindow { + private enum LiveLogFilterMode + { + All, + HasListeners, + NoListeners + } + + private sealed class HandlerDisplayData + { + public object TargetInstance { get; set; } + public MonoBehaviour PingTarget { get; set; } + public string TargetName { get; set; } + public string MethodName { get; set; } + public object Priority { get; set; } + public string ExecType { get; set; } + public bool IsStaticHandler { get; set; } + } + + private sealed class EventLogRecord + { + public DateTime Timestamp { get; set; } + public string TimestampText { get; set; } + public string EventName { get; set; } + public string SenderInfo { get; set; } + public bool HasListeners { get; set; } + public bool IsCancelable { get; set; } + public bool IsCanceled { get; set; } + public string Result { get; set; } + public string Phase { get; set; } + public List Parameters { get; set; } + public List Handlers { get; set; } + public string MatchText { get; set; } + } + + private sealed class EventLogGroup + { + public string EventName { get; set; } + public List Records { get; } = new(); + public EventLogRecord LatestRecord => Records[0]; + } + private ScrollView _subscribersScrollView; private ScrollView _liveTrackerScrollView; private VisualElement _subscribersTab; private VisualElement _liveTrackerTab; private Label _instanceCountLabel; + private Label _liveLogStatsLabel; private string _searchString = ""; + private string _liveLogSearchString = ""; + private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All; + private bool _collapseSameEventType = true; private const int MaxLogCount = 100; - private readonly List _logElements = new(); + private readonly List _logRecords = new(); - [MenuItem("Window/Shrink EventBus/事件查看器")] + [MenuItem("ShrinkSDK/事件总线/事件查看器")] public static void ShowWindow() { var window = GetWindow("EventBus 控制台"); @@ -117,10 +162,65 @@ namespace ShrinkEventBus.Editor { var container = new VisualElement { style = { flexGrow = 1 } }; var toolbar = new Toolbar(); + var searchField = new ToolbarSearchField { style = { flexGrow = 1 } }; + searchField.RegisterValueChangedCallback(evt => + { + _liveLogSearchString = evt.newValue ?? string.Empty; + RefreshLiveTrackerView(); + }); + toolbar.Add(searchField); + + var filterMenu = new ToolbarMenu { text = "全部事件" }; + filterMenu.menu.AppendAction("全部事件", _ => + { + _liveLogFilterMode = LiveLogFilterMode.All; + filterMenu.text = "全部事件"; + RefreshLiveTrackerView(); + }); + filterMenu.menu.AppendAction("仅有监听者", _ => + { + _liveLogFilterMode = LiveLogFilterMode.HasListeners; + filterMenu.text = "仅有监听者"; + RefreshLiveTrackerView(); + }); + filterMenu.menu.AppendAction("仅无监听者", _ => + { + _liveLogFilterMode = LiveLogFilterMode.NoListeners; + filterMenu.text = "仅无监听者"; + RefreshLiveTrackerView(); + }); + toolbar.Add(filterMenu); + + var collapseToggle = new ToolbarToggle + { + text = "折叠同类事件", + value = _collapseSameEventType + }; + collapseToggle.RegisterValueChangedCallback(evt => + { + _collapseSameEventType = evt.newValue; + RefreshLiveTrackerView(); + }); + toolbar.Add(collapseToggle); toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" }); container.Add(toolbar); + + _liveLogStatsLabel = new Label + { + style = + { + marginLeft = 8, + marginRight = 8, + marginTop = 4, + marginBottom = 4, + color = new StyleColor(Color.gray) + } + }; + container.Add(_liveLogStatsLabel); + _liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } }; container.Add(_liveTrackerScrollView); + RefreshLiveTrackerView(); return container; } @@ -176,76 +276,232 @@ namespace ShrinkEventBus.Editor private void RecordEventLog(EventBase evt, string eventName, string senderInfo) { if (!EventBus.EnableDebugRecord) return; + _logRecords.Insert(0, CreateEventLogRecord(evt, eventName, senderInfo)); + if (_logRecords.Count > MaxLogCount) + _logRecords.RemoveAt(_logRecords.Count - 1); + RefreshLiveTrackerView(); + } + private EventLogRecord CreateEventLogRecord(EventBase evt, string eventName, string senderInfo) + { + var timestamp = DateTime.Now; var list = evt.GetListenerList(); - var hasListeners = list != null && list.Count > 0; - - var logEntry = new VisualElement - { - style = - { - flexDirection = FlexDirection.Column, paddingBottom = 4, paddingTop = 4, borderBottomWidth = 1, - borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f)) - } - }; - - var headerRow = new VisualElement - { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; - headerRow.Add(new Label(DateTime.Now.ToString("HH:mm:ss.fff")) - { style = { width = 80, color = new StyleColor(Color.gray) } }); - headerRow.Add(new Label(eventName) - { - style = { color = new StyleColor(new Color(1f, 0.84f, 0f)), unityFontStyleAndWeight = FontStyle.Bold } - }); - - var senderClassName = senderInfo.Contains(".") ? senderInfo.Split('.')[0] : senderInfo; - var senderLabel = new Label($" [由 {senderInfo} 触发]") - { - style = - { - color = new StyleColor(new Color(0.5f, 0.8f, 1f)), marginLeft = 5, - unityFontStyleAndWeight = FontStyle.Italic - } - }; - - if (senderClassName != "未知来源" && senderClassName != "EventBus") - { - senderLabel.RegisterCallback(e => - senderLabel.style.color = new StyleColor(Color.cyan)); - senderLabel.RegisterCallback(e => - senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f))); - senderLabel.RegisterCallback(e => OpenScriptByClassName(senderClassName)); - } - - headerRow.Add(senderLabel); - logEntry.Add(headerRow); + var handlers = list?.GetHandlers() ?? Array.Empty(); + var hasListeners = handlers.Length > 0; + var paramsList = DumpEventParams(evt); + var handlerRows = new List(handlers.Length); + foreach (var handler in handlers) + handlerRows.Add(ExtractHandlerDisplayData(handler)); var isCancelable = (GetMemberValue(evt, "IsCancelable") as bool?) ?? false; var isCanceled = (GetMemberValue(evt, "IsCanceled") as bool?) ?? false; var resultStr = GetMemberValue(evt, "Result")?.ToString() ?? "DEFAULT"; var phaseStr = GetMemberValue(evt, "Phase")?.ToString() ?? "NORMAL"; + return new EventLogRecord + { + Timestamp = timestamp, + TimestampText = timestamp.ToString("HH:mm:ss.fff"), + EventName = eventName, + SenderInfo = senderInfo, + HasListeners = hasListeners, + IsCancelable = isCancelable, + IsCanceled = isCanceled, + Result = resultStr, + Phase = phaseStr, + Parameters = paramsList, + Handlers = handlerRows, + MatchText = BuildLogMatchText(eventName, senderInfo, paramsList) + }; + } + + private string BuildLogMatchText(string eventName, string senderInfo, List paramsList) + { + if (paramsList.Count == 0) + return $"{eventName}\n{senderInfo}"; + return $"{eventName}\n{senderInfo}\n{string.Join("\n", paramsList)}"; + } + + private void RefreshLiveTrackerView() + { + if (_liveTrackerScrollView == null) return; + + _liveTrackerScrollView.Clear(); + + var filteredRecords = new List(); + foreach (var record in _logRecords) + { + if (MatchesLiveLogFilter(record)) + filteredRecords.Add(record); + } + + if (_liveLogStatsLabel != null) + { + _liveLogStatsLabel.text = _collapseSameEventType + ? $"显示 {filteredRecords.Count} / {_logRecords.Count} 条,按事件类型折叠" + : $"显示 {filteredRecords.Count} / {_logRecords.Count} 条"; + } + + if (filteredRecords.Count == 0) + { + _liveTrackerScrollView.Add(new HelpBox("当前没有符合条件的实时事件日志。", HelpBoxMessageType.Info)); + return; + } + + if (_collapseSameEventType) + { + foreach (var group in BuildCollapsedGroups(filteredRecords)) + _liveTrackerScrollView.Add(CreateCollapsedGroupElement(group)); + return; + } + + foreach (var record in filteredRecords) + _liveTrackerScrollView.Add(CreateLogEntryElement(record)); + } + + private bool MatchesLiveLogFilter(EventLogRecord record) + { + switch (_liveLogFilterMode) + { + case LiveLogFilterMode.HasListeners: + if (!record.HasListeners) return false; + break; + case LiveLogFilterMode.NoListeners: + if (record.HasListeners) return false; + break; + } + + if (string.IsNullOrWhiteSpace(_liveLogSearchString)) + return true; + + return record.MatchText.IndexOf(_liveLogSearchString.Trim(), StringComparison.OrdinalIgnoreCase) >= 0; + } + + private List BuildCollapsedGroups(List records) + { + var groups = new List(); + var groupMap = new Dictionary(StringComparer.Ordinal); + + foreach (var record in records) + { + if (!groupMap.TryGetValue(record.EventName, out var group)) + { + group = new EventLogGroup { EventName = record.EventName }; + groupMap[record.EventName] = group; + groups.Add(group); + } + + group.Records.Add(record); + } + + return groups; + } + + private VisualElement CreateCollapsedGroupElement(EventLogGroup group) + { + var latest = group.LatestRecord; + var foldout = new Foldout + { + text = $"{group.EventName} × {group.Records.Count} 最近: {latest.TimestampText}", + value = group.Records.Count == 1, + style = + { + backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.4f)), + marginTop = 2, + paddingBottom = 2, + borderBottomWidth = 1, + borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f)) + } + }; + + var summary = new Label( + $"最近来源: {latest.SenderInfo} | 监听者: {(latest.HasListeners ? latest.Handlers.Count : 0)} | 已取消: {(latest.IsCanceled ? "是" : "否")}") + { + style = + { + marginLeft = 18, + marginBottom = 4, + color = new StyleColor(Color.gray) + } + }; + foldout.Add(summary); + + foreach (var record in group.Records) + { + var row = CreateLogEntryElement(record); + row.style.marginLeft = 12; + foldout.Add(row); + } + + return foldout; + } + + private VisualElement CreateLogEntryElement(EventLogRecord record) + { + var logEntry = new VisualElement + { + style = + { + flexDirection = FlexDirection.Column, + paddingBottom = 4, + paddingTop = 4, + borderBottomWidth = 1, + borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f)) + } + }; + + var headerRow = new VisualElement + { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; + headerRow.Add(new Label(record.TimestampText) + { style = { width = 80, color = new StyleColor(Color.gray) } }); + headerRow.Add(new Label(record.EventName) + { + style = { color = new StyleColor(new Color(1f, 0.84f, 0f)), unityFontStyleAndWeight = FontStyle.Bold } + }); + + var senderClassName = GetSenderClassName(record.SenderInfo); + var senderLabel = new Label($" [由 {record.SenderInfo} 触发]") + { + style = + { + color = new StyleColor(new Color(0.5f, 0.8f, 1f)), + marginLeft = 5, + unityFontStyleAndWeight = FontStyle.Italic + } + }; + + if (senderClassName != "未知来源" && senderClassName != "EventBus" && senderClassName != "Unknown") + { + senderLabel.RegisterCallback(_ => + senderLabel.style.color = new StyleColor(Color.cyan)); + senderLabel.RegisterCallback(_ => + senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f))); + senderLabel.RegisterCallback(_ => OpenScriptByClassName(senderClassName)); + } + + headerRow.Add(senderLabel); + logEntry.Add(headerRow); + var statusRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } }; - statusRow.Add(new Label($"可取消: {(isCancelable ? "是" : "否")}") + statusRow.Add(new Label($"可取消: {(record.IsCancelable ? "是" : "否")}") { style = { color = new StyleColor(Color.gray), marginRight = 15 } }); - var canceledLabel = new Label($"已取消: {(isCanceled ? "是" : "否")}") { style = { marginRight = 15 } }; + var canceledLabel = new Label($"已取消: {(record.IsCanceled ? "是" : "否")}") + { style = { marginRight = 15 } }; canceledLabel.style.color = - isCanceled ? new StyleColor(new Color(1f, 0.3f, 0.3f)) : new StyleColor(Color.gray); + record.IsCanceled ? new StyleColor(new Color(1f, 0.3f, 0.3f)) : new StyleColor(Color.gray); statusRow.Add(canceledLabel); - statusRow.Add(new Label($"Result: {resultStr}") + statusRow.Add(new Label($"Result: {record.Result}") { style = { color = new StyleColor(new Color(0.8f, 0.6f, 0.8f)), marginRight = 15 } }); - statusRow.Add(new Label($"最后阶段: {phaseStr}") + statusRow.Add(new Label($"最后阶段: {record.Phase}") { style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } }); logEntry.Add(statusRow); var paramsContainer = new VisualElement { style = { flexDirection = FlexDirection.Column, marginLeft = 80, marginTop = 2, marginBottom = 2 } }; - var paramsList = DumpEventParams(evt); - - if (paramsList.Count == 0) + if (record.Parameters.Count == 0) { paramsContainer.Add(new Label("参数: { 无 }") { style = { color = new StyleColor(new Color(0.5f, 0.5f, 0.5f)) } }); @@ -254,24 +510,17 @@ namespace ShrinkEventBus.Editor { paramsContainer.Add( new Label("携带参数:") { style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } }); - foreach (var pStr in paramsList) + foreach (var pStr in record.Parameters) paramsContainer.Add(new Label($" • {pStr}") { style = { color = new StyleColor(new Color(0.6f, 0.9f, 0.6f)), marginLeft = 10 } }); } logEntry.Add(paramsContainer); - if (hasListeners) + if (record.HasListeners) { - if (list.GetType().GetMethod("GetHandlers")?.Invoke(list, null) is Array handlers) - { - foreach (var h in handlers) - { - var row = CreateHandlerRow(h); - row.style.marginLeft = 80; - logEntry.Add(row); - } - } + foreach (var handler in record.Handlers) + logEntry.Add(CreateHandlerRow(handler, 80)); } else { @@ -281,26 +530,34 @@ namespace ShrinkEventBus.Editor logEntry.Add(noHandlerRow); } - _liveTrackerScrollView.Insert(0, logEntry); - _logElements.Insert(0, logEntry); + return logEntry; + } - if (_logElements.Count > MaxLogCount) - { - _liveTrackerScrollView.Remove(_logElements[^1]); - _logElements.RemoveAt(_logElements.Count - 1); - } + private string GetSenderClassName(string senderInfo) + { + if (string.IsNullOrWhiteSpace(senderInfo)) + return "未知来源"; + return senderInfo.Contains(".") ? senderInfo.Split('.')[0] : senderInfo; } private VisualElement CreateHandlerRow(object h) { - ParseHandlerData(h, out var targetInstance, out var targetName, out var methodName, out var priority, - out var execType); + return CreateHandlerRow(ExtractHandlerDisplayData(h), 40); + } + + private VisualElement CreateHandlerRow(HandlerDisplayData handlerData, float marginLeft) + { + var targetInstance = handlerData.TargetInstance; + var targetName = handlerData.TargetName; + var methodName = handlerData.MethodName; + var priority = handlerData.Priority; + var execType = handlerData.ExecType; var row = new VisualElement { style = { - flexDirection = FlexDirection.Row, marginTop = 2, marginBottom = 2, marginLeft = 40, + flexDirection = FlexDirection.Row, marginTop = 2, marginBottom = 2, marginLeft = marginLeft, alignItems = Align.Center } }; @@ -318,16 +575,16 @@ namespace ShrinkEventBus.Editor linkLabel.style.flexGrow = 1; row.Add(linkLabel); - if (targetInstance is MonoBehaviour mb && mb != null) + if (handlerData.PingTarget) { var pingBtn = new Button(() => { - EditorGUIUtility.PingObject(mb.gameObject); - Selection.activeGameObject = mb.gameObject; + EditorGUIUtility.PingObject(handlerData.PingTarget.gameObject); + Selection.activeGameObject = handlerData.PingTarget.gameObject; }) { text = "Ping", style = { width = 50, height = 18 } }; row.Add(pingBtn); } - else if (targetInstance == null && methodName != "未知方法") + else if (handlerData.IsStaticHandler && methodName != "未知方法") row.Add(new Label("(静态)") { style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight } @@ -341,19 +598,18 @@ namespace ShrinkEventBus.Editor return row; } - private void ParseHandlerData(object handlerObj, out object targetInstance, out string targetName, - out string methodName, out object priority, out string execType) + private HandlerDisplayData ExtractHandlerDisplayData(object handlerObj) { - priority = GetMemberValue(handlerObj, "Priority") ?? "NORMAL"; + var priority = GetMemberValue(handlerObj, "Priority") ?? "NORMAL"; var debugInfo = GetMemberValue(handlerObj, "DebugInfo") as string ?? "未知方法"; - execType = "Sync"; + var execType = "Sync"; if (debugInfo.Contains("(UniTask)") || debugInfo.Contains("Async")) execType = "Async"; else if (debugInfo.Contains("(Task->UniTask)") || debugInfo.Contains("(Task)")) execType = "Task"; - targetInstance = null; - targetName = "未知类"; - methodName = debugInfo; + object targetInstance = null; + var targetName = "未知类"; + var methodName = debugInfo; var handlerDelegate = GetMemberValue(handlerObj, "Handler") as Delegate; if (handlerDelegate != null) @@ -379,6 +635,17 @@ namespace ShrinkEventBus.Editor methodName = methodInfo.Name; } } + + return new HandlerDisplayData + { + TargetInstance = targetInstance, + PingTarget = targetInstance as MonoBehaviour, + TargetName = targetName, + MethodName = methodName, + Priority = priority, + ExecType = execType, + IsStaticHandler = targetInstance == null + }; } private List DumpEventParams(EventBase evt) @@ -446,8 +713,8 @@ namespace ShrinkEventBus.Editor private void ClearLogs() { - _liveTrackerScrollView.Clear(); - _logElements.Clear(); + _logRecords.Clear(); + RefreshLiveTrackerView(); } private void OpenScriptByClassName(string className) @@ -470,4 +737,4 @@ namespace ShrinkEventBus.Editor } } } -#endif \ No newline at end of file +#endif diff --git a/README.md b/README.md index 10684c5..eb2063a 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,14 @@ EventBus.TriggerEvent(evt); ## 🖼️ 追踪图形化 -菜单栏 → `Window(窗口)` → `Shrink EventBus` → `事件查看器` +菜单栏 → `ShrinkSDK` → `事件总线` → `事件查看器` ![订阅者全览](img1.png) ![实时触发日志](img2.png) +实时触发日志页现已支持关键词过滤、按“有监听者 / 无监听者”筛选,以及“折叠同类事件”聚合查看,适合排查高频事件刷屏场景。 + --- ## 📖 核心概念 @@ -292,14 +294,14 @@ EventBus.UnregisterAllEvents(); // 全部清空(谨慎 #### 触发 ```csharp -// 同步触发:async handler 会以 .Forget() 方式触发,不等待 +// 同步触发:只同步等待 sync handler;async handler 会基于事件快照 fire-and-forget bool handled = EventBus.TriggerEvent(TEvent eventArgs); // 异步触发:顺序 await 每个 handler bool handled = await EventBus.TriggerEventAsync(TEvent eventArgs); ``` -> ⚠️ `TriggerEvent` 中遇到 async handler 时会调用 `.Forget()`,不会等待其完成。如果需要顺序等待,请使用 `TriggerEventAsync`。 +> ⚠️ `TriggerEvent` 中遇到 async handler 时,不会等待其完成,而是对当前事件做一份快照后异步执行。如果你需要让 async handler 参与最终状态(如 `IsCanceled` / `Result` / 后续字段改写),请使用 `TriggerEventAsync`。 #### 查询 @@ -375,7 +377,7 @@ TriggerEvent(evt) └─ 遍历 handlers[] ├─ 跳过已取消 & 不接收取消的 handler ├─ Action → 直接调用 - └─ Func → .Forget()(同步路径) + └─ Func → 基于事件快照 .Forget()(同步路径) ``` **自动注册完整流程:** @@ -452,7 +454,8 @@ public void Dispose() ## ⚠️ 注意事项 -- **`TriggerEvent` 不等待异步 handler**:同步路径中的 UniTask handler 以 `.Forget()` 触发,执行结果和异常不会传回调用方。需要等待时请使用 `TriggerEventAsync`。 +- **`TriggerEvent` 不等待异步 handler**:同步路径中的 UniTask handler 会基于事件快照异步执行,执行结果和异常不会传回调用方,对原事件对象的改动也不会回写。需要等待并拿到最终状态时请使用 `TriggerEventAsync`。 +- **同步路径中的 async 快照是浅拷贝**:事件对象本身会复制一份,但如果载荷里挂着可变引用类型(如 `List<>`、`Dictionary<>`、自定义引用对象),内部成员仍然是共享引用。高风险数据建议改成不可变载荷,或统一走 `TriggerEventAsync`。 - **EventPool 归还后不要再使用**:`Release` 后对象会立即 `ResetInternal()`,继续访问属性将得到默认值。 - **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 快照,会产生语义上的不确定性。 - **静态 handler 永远不会自动注销**:静态方法注册后持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。 @@ -488,7 +491,7 @@ foreach (var h in handlers) **Editor 下想追踪事件流** -打开事件查看器:菜单栏 → `Window` → `Shrink EventBus` → `事件查看器` +打开事件查看器:菜单栏 → `ShrinkSDK` → `事件总线` → `事件查看器` 也可以通过代码追踪: @@ -503,4 +506,4 @@ foreach (var h in evt.GetSubscribers()) ## 📄 License -[MIT](LICENSE) \ No newline at end of file +[MIT](LICENSE) diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..e8ffa9c --- /dev/null +++ b/Runtime/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")] diff --git a/Runtime/AssemblyInfo.cs.meta b/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..58597f6 --- /dev/null +++ b/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43e85f78c24f1d94ba7d06a4fa37e937 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/EventBase.cs b/Runtime/EventBase.cs index c146cc5..189b317 100644 --- a/Runtime/EventBase.cs +++ b/Runtime/EventBase.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Reflection; +using System.Runtime.Serialization; namespace ShrinkEventBus { @@ -10,16 +11,26 @@ namespace ShrinkEventBus 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 { get; private set; } = 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 ReleaseAction { get; set; } + internal Action? ReleaseAction { get; set; } protected EventBase() { @@ -44,6 +55,16 @@ namespace ShrinkEventBus OnReset(); } + internal void PrepareForDispatch() + { + CurrentHandler = null; + Phase = null; + EventTime = DateTime.UtcNow; + EventId = Guid.NewGuid(); + _listenerList.Clear(); + } + + [IgnoreDataMember] public bool IsCanceled { get => _isCanceled; @@ -54,6 +75,7 @@ namespace ShrinkEventBus } } + [IgnoreDataMember] public EventResult Result { get => _result; @@ -82,4 +104,4 @@ namespace ShrinkEventBus ReleaseAction?.Invoke(this); } } -} \ No newline at end of file +} diff --git a/Runtime/EventBus.cs b/Runtime/EventBus.cs index 36ed365..d04e4be 100644 --- a/Runtime/EventBus.cs +++ b/Runtime/EventBus.cs @@ -9,6 +9,8 @@ namespace ShrinkEventBus { public static class EventBus { + public static event Action OnEventTriggered; + #if UNITY_EDITOR public static bool EnableDebugRecord { get; set; } public static event Action OnEventTriggeredForEditor; @@ -189,16 +191,11 @@ namespace ShrinkEventBus public static async UniTask TriggerEventAsync(TEvent eventArgs) where TEvent : EventBase { EnsureAutoManagerInitialized(); + eventArgs.PrepareForDispatch(); var collection = EventCache.List; - if (collection == null) return false; - - var handlers = collection.GetHandlers(); + var handlers = collection?.GetHandlers() ?? Array.Empty(); var wasHandled = false; -#if UNITY_EDITOR - RecordEditorTrace(handlers, eventArgs, typeof(TEvent)); -#endif - for (var i = 0; i < handlers.Length; i++) { var handlerInfo = handlers[i]; @@ -225,6 +222,7 @@ namespace ShrinkEventBus } eventArgs.CurrentHandler = null; + CompleteEventDispatch(eventArgs, typeof(TEvent), handlers); return wasHandled; } @@ -232,16 +230,11 @@ namespace ShrinkEventBus public static bool TriggerEvent(TEvent eventArgs) where TEvent : EventBase { EnsureAutoManagerInitialized(); + eventArgs.PrepareForDispatch(); var collection = EventCache.List; - if (collection == null) return false; - - var handlers = collection.GetHandlers(); + var handlers = collection?.GetHandlers() ?? Array.Empty(); var wasHandled = false; -#if UNITY_EDITOR - RecordEditorTrace(handlers, eventArgs, typeof(TEvent)); -#endif - for (var i = 0; i < handlers.Length; i++) { var handlerInfo = handlers[i]; @@ -256,7 +249,8 @@ namespace ShrinkEventBus } else if (handlerInfo.Handler is Func asyncHandler) { - FireAndForgetSafe(() => asyncHandler(eventArgs), + var detachedEvent = EventCloneUtility.CloneForDetachedDispatch(eventArgs); + FireAndForgetSafe(() => asyncHandler(detachedEvent), $"{handlerInfo.DisplayDeclaringType.Name}.{handlerInfo.DisplayMethodName}"); wasHandled = true; } @@ -269,6 +263,7 @@ namespace ShrinkEventBus } eventArgs.CurrentHandler = null; + CompleteEventDispatch(eventArgs, typeof(TEvent), handlers); return wasHandled; } @@ -281,15 +276,26 @@ namespace ShrinkEventBus }); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CompleteEventDispatch(EventBase eventArgs, Type eventType, EventHandlerInfo[] handlers) + { + OnEventTriggered?.Invoke(eventArgs, eventType); + #if UNITY_EDITOR - private static void RecordEditorTrace(EventHandlerInfo[] handlers, TEvent eventArgs, Type eventType) - where TEvent : EventBase + 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]); - OnEventTriggeredForEditor?.Invoke(eventArgs, eventType.Name, GetSenderInfo()); } private static string GetSenderInfo() @@ -389,4 +395,4 @@ namespace ShrinkEventBus } } } -} \ No newline at end of file +} diff --git a/Runtime/EventCloneUtility.cs b/Runtime/EventCloneUtility.cs new file mode 100644 index 0000000..393f92a --- /dev/null +++ b/Runtime/EventCloneUtility.cs @@ -0,0 +1,85 @@ +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 FieldCache = new(); + + public static TEvent CloneForDetachedDispatch(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."); + + CopyFields(source, clone); + CopyListenerSnapshot(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 void CopyListenerSnapshot(EventBase source, EventBase target) + { + var targetListeners = target.GetListenerList(); + targetListeners.Clear(); + + var sourceHandlers = source.GetListenerList().GetHandlers(); + for (var i = 0; i < sourceHandlers.Length; i++) + targetListeners.Add(sourceHandlers[i]); + } + + private static FieldInfo[] GetCopyableFields(Type type) + { + lock (CacheLock) + { + if (FieldCache.TryGetValue(type, out var cached)) + return cached; + + var fields = new List(); + 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 "_listenerList" + or "k__BackingField" + or "k__BackingField"; + } + } +} diff --git a/Runtime/EventCloneUtility.cs.meta b/Runtime/EventCloneUtility.cs.meta new file mode 100644 index 0000000..489c6b8 --- /dev/null +++ b/Runtime/EventCloneUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 876564c111495b2489870b29ab75da5e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 7b4f3cf..5328b23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-eventbus", - "version": "1.1.4", + "version": "1.1.5", "displayName": "ShrinkEventBus", "description": "高性能、类型安全的 Unity 事件总线,支持优先级调度与自动注册。", "unity": "2022.3", @@ -15,4 +15,4 @@ "name": "cneicy", "url": "https://github.com/cneicy" } -} \ No newline at end of file +}