From 541053c17030a10bb1a27d04ff8b45cd1569ebc3 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Wed, 29 Jul 2026 19:32:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E6=A0=BC=E5=9C=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=A0=E9=99=A4=E6=96=B9=E9=9D=A2=E7=9A=84?= =?UTF-8?q?=E7=94=A8=E6=B3=95=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BA=86CheckUpda?= =?UTF-8?q?te=E5=87=BD=E6=95=B0=E7=94=A8=E4=BA=8E=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=A0=88=EF=BC=8C=E5=90=88=E5=B9=B6=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=B0=83=E7=94=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Component/XericUIActionTable.Api.cs | 15 +- .../Component/XericUIActionTable.Editor.cs | 2 +- .../Component/XericUIActionTable.ExcelIO.cs | 5 +- .../XericUIActionTable.RendererFilter.cs | 32 ++++ .../XericUIActionTable.UpdateScope.cs | 164 ++++++++++++++++++ .../XericUIActionTable.UpdateScope.cs.meta | 11 ++ .../Rendering/Component/XericUIActionTable.cs | 13 +- .../Rendering/Elements/PrefabPrototypePool.cs | 16 ++ 8 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs create mode 100644 Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs.meta diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs index 8d8b5ff..7283c7d 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs @@ -194,6 +194,7 @@ namespace XericUI.XTable.Rendering.Component System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length); newArr[newLen - 1] = height; m_RowHeights = newArr; + RecordStructuralChange(false); MarkDirty(TableDirtyType.LayoutChanged); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); @@ -212,6 +213,7 @@ namespace XericUI.XTable.Rendering.Component System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length); newArr[newLen - 1] = width; m_ColWidths = newArr; + RecordStructuralChange(false); MarkDirty(TableDirtyType.LayoutChanged); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); @@ -256,6 +258,8 @@ namespace XericUI.XTable.Rendering.Component UnityEditor.Undo.RecordObject(this, "Remove Table Row"); #endif int colCount = m_ColWidths.Length; + CaptureActivePrefabReleaseCallbacks(); + RecordStructuralChange(true); // 先解除涉及该行的合并 for (int c = 0; c < colCount; c++) @@ -303,6 +307,8 @@ namespace XericUI.XTable.Rendering.Component UnityEditor.Undo.RecordObject(this, "Remove Table Column"); #endif int rowCount = m_RowHeights.Length; + CaptureActivePrefabReleaseCallbacks(); + RecordStructuralChange(true); // 先解除涉及该列的合并 for (int r = 0; r < rowCount; r++) @@ -410,7 +416,8 @@ namespace XericUI.XTable.Rendering.Component private void OnHandleSelectionChanged(XTableSelectionHandle handle) { MarkDirty(TableDirtyType.SelectionChanged); - RefreshSelectionOverlays(); + if (!IsCheckingUpdate) + RefreshSelectionOverlays(); } /// 裁剪选区到当前行列尺寸范围内 @@ -562,6 +569,7 @@ namespace XericUI.XTable.Rendering.Component /// public void ClearTable() { + ThrowIfCheckingUpdate(nameof(ClearTable)); m_IsRebuilding = true; try { @@ -624,6 +632,8 @@ namespace XericUI.XTable.Rendering.Component public void ApplyClipboardData(XTableClipboardData data) { if (data == null) return; + CaptureActivePrefabReleaseCallbacks(); + RecordStructuralChange(true); // 确保数据层存在 if (m_TableData == null) @@ -640,9 +650,8 @@ namespace XericUI.XTable.Rendering.Component // 写回单元格文本与合并信息 data.PopulateToTable(this); - MarkDirty(TableDirtyType.DataChanged); RecalculateTotalSize(); - RefreshView(); + RequestRefresh(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged, true); SetChildrenVisible(m_ChildrenVisible); } } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs index c1f51b3..f00fbc7 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Editor.cs @@ -39,7 +39,7 @@ namespace XericUI.XTable.Rendering.Component return; } - if (m_IsRebuilding || !m_DirtyFlag.IsDirty) return; + if (m_IsRebuilding || IsCheckingUpdate || !m_DirtyFlag.IsDirty) return; var flags = m_DirtyFlag.Flags; if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0) diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs index b7d192b..6620fd6 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.ExcelIO.cs @@ -262,9 +262,10 @@ namespace XericUI.XTable.Rendering.Component } // 标记脏并刷新 - MarkDirty(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged); + CaptureActivePrefabReleaseCallbacks(); + RecordStructuralChange(true); RecalculateTotalSize(); - RefreshView(); + RequestRefresh(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged, true); } #endregion diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index 91187b3..77572ab 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -225,6 +225,37 @@ namespace XericUI.XTable.Rendering.Component #region 完整刷新 + private void CommitStructuralRenderCleanup(bool invalidatePrefabBindings, + Dictionary> prefabReleaseCallbacks) + { + ReleaseAllTrackedCells(); + ReleaseFrozenCells(); + m_LastRenderedCells.Clear(); + + if (m_ActivePrefabMap != null) + { + foreach (var pair in m_ActivePrefabMap) + { + if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.ContainsKey(pair.Value)) continue; + var data = TableData.GetCell(pair.Key.Item1, pair.Key.Item2); + if (data?.OnPrefabRelease != null && prefabReleaseCallbacks != null + && !prefabReleaseCallbacks.ContainsKey(pair.Value)) + prefabReleaseCallbacks.Add(pair.Value, data.OnPrefabRelease); + } + m_ActivePrefabMap.Clear(); + } + + if (invalidatePrefabBindings) + m_PrefabPool?.ReleaseAll(instance => + { + if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.TryGetValue(instance, out var callback)) + callback(instance); + }); + + m_QuadtreeDirty = true; + m_StyleNamespaceDirty = true; + } + public void RefreshView() { if (m_Assembler == null) @@ -300,6 +331,7 @@ namespace XericUI.XTable.Rendering.Component private void RefreshViewLight() { + TableDirtyType refreshedFlags = m_DirtyFlag.Flags; if (m_Assembler == null) { if (rectTransform != null) diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs new file mode 100644 index 0000000..4fb2523 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.XTable.Rendering.Component +{ + public partial class XericUIActionTable + { + private sealed class UpdateContext + { + public TableDirtyType DirtyFlags; + public bool HasStructuralChange; + public bool InvalidatePrefabBindings; + public bool RequiresImmediateRefresh; + public readonly Dictionary> PrefabReleaseCallbacks = new(); + + public void Reset() + { + DirtyFlags = TableDirtyType.None; + HasStructuralChange = false; + InvalidatePrefabBindings = false; + RequiresImmediateRefresh = false; + PrefabReleaseCallbacks.Clear(); + } + + public void MergeFrom(UpdateContext child) + { + DirtyFlags |= child.DirtyFlags; + HasStructuralChange |= child.HasStructuralChange; + InvalidatePrefabBindings |= child.InvalidatePrefabBindings; + RequiresImmediateRefresh |= child.RequiresImmediateRefresh; + foreach (var pair in child.PrefabReleaseCallbacks) + if (!PrefabReleaseCallbacks.ContainsKey(pair.Key)) + PrefabReleaseCallbacks.Add(pair.Key, pair.Value); + } + } + + public sealed class UpdateScope : IDisposable + { + private XericUIActionTable m_Table; + private readonly long m_Token; + private bool m_Disposed; + + internal UpdateScope(XericUIActionTable table, long token) + { + m_Table = table; + m_Token = token; + } + + public void Dispose() + { + if (m_Disposed) return; + m_Disposed = true; + if (m_Table != null) + m_Table.EndCheckUpdate(m_Token); + m_Table = null; + } + } + + [NonSerialized] private readonly Stack m_UpdateContextStack = new(); + [NonSerialized] private readonly Stack m_UpdateContextPool = new(); + [NonSerialized] private long m_NextUpdateToken; + [NonSerialized] private readonly Stack m_UpdateTokenStack = new(); + + private bool IsCheckingUpdate => m_UpdateContextStack.Count > 0; + + /// + /// 开始一个可嵌套的批量更新作用域。作用域内的数据修改立即生效,渲染与结构资源整理延迟到最外层 Dispose。 + /// + public UpdateScope CheckUpdate() + { + var context = m_UpdateContextPool.Count > 0 ? m_UpdateContextPool.Pop() : new UpdateContext(); + context.Reset(); + m_UpdateContextStack.Push(context); + long token = ++m_NextUpdateToken; + m_UpdateTokenStack.Push(token); + return new UpdateScope(this, token); + } + + private void EndCheckUpdate(long token) + { + if (m_UpdateTokenStack.Count == 0 || m_UpdateTokenStack.Peek() != token) + { + Debug.LogError("[XericUIActionTable] CheckUpdate 作用域必须按 LIFO 顺序释放。", this); + return; + } + + m_UpdateTokenStack.Pop(); + UpdateContext context = m_UpdateContextStack.Pop(); + if (m_UpdateContextStack.Count > 0) + { + m_UpdateContextStack.Peek().MergeFrom(context); + ReturnUpdateContext(context); + return; + } + + try + { + CommitUpdateContext(context); + } + finally + { + ReturnUpdateContext(context); + } + } + + private void ReturnUpdateContext(UpdateContext context) + { + context.Reset(); + m_UpdateContextPool.Push(context); + } + + private void CommitUpdateContext(UpdateContext context) + { + if (context.DirtyFlags == TableDirtyType.None) return; + + if (context.HasStructuralChange) + CommitStructuralRenderCleanup(context.InvalidatePrefabBindings, context.PrefabReleaseCallbacks); + + ApplyDirty(context.DirtyFlags); + if (context.RequiresImmediateRefresh) + RefreshView(); + RefreshSelectionOverlays(); + } + + private void RecordStructuralChange(bool invalidatePrefabBindings) + { + if (!IsCheckingUpdate) return; + UpdateContext context = m_UpdateContextStack.Peek(); + context.HasStructuralChange = true; + context.InvalidatePrefabBindings |= invalidatePrefabBindings; + } + + private void CaptureActivePrefabReleaseCallbacks() + { + if (!IsCheckingUpdate || m_ActivePrefabMap == null) return; + var callbacks = m_UpdateContextStack.Peek().PrefabReleaseCallbacks; + foreach (var pair in m_ActivePrefabMap) + { + var callback = TableData.GetCell(pair.Key.Item1, pair.Key.Item2)?.OnPrefabRelease; + if (callback != null && !callbacks.ContainsKey(pair.Value)) + callbacks.Add(pair.Value, callback); + } + } + + private void RequestRefresh(TableDirtyType type, bool immediate) + { + MarkDirty(type); + if (IsCheckingUpdate) + { + m_UpdateContextStack.Peek().RequiresImmediateRefresh |= immediate; + return; + } + if (immediate) + RefreshView(); + } + + private void ThrowIfCheckingUpdate(string operation) + { + if (IsCheckingUpdate) + throw new InvalidOperationException($"{operation} 不能在 CheckUpdate 作用域内调用。"); + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs.meta new file mode 100644 index 0000000..533592f --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28fb43aa1d17ba14b9fa4cf1bd2ad055 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index 12c0bcb..74c356a 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -347,7 +347,7 @@ namespace XericUI.XTable.Rendering.Component private void Update() { UpdateSelectionAutoScroll(); - if (m_IsRebuilding || !m_DirtyFlag.IsDirty) return; + if (m_IsRebuilding || IsCheckingUpdate || !m_DirtyFlag.IsDirty) return; var flags = m_DirtyFlag.Flags; // 仅滚动或选区变更 → 轻刷新(不重建边框/背景/预制体全回收) @@ -385,6 +385,16 @@ namespace XericUI.XTable.Rendering.Component #region 脏标记与滚动回调 public void MarkDirty(TableDirtyType type) + { + if (IsCheckingUpdate) + { + m_UpdateContextStack.Peek().DirtyFlags |= type; + return; + } + ApplyDirty(type); + } + + private void ApplyDirty(TableDirtyType type) { m_DirtyFlag.Mark(type); @@ -402,6 +412,7 @@ namespace XericUI.XTable.Rendering.Component private void OnScrollValueChanged(Vector2 _) { + if (IsCheckingUpdate) return; // 防重入:RefreshViewLight 中操作 content 可能触发 ScrollRect 再次回调 if (m_IsRefreshingFromScroll) return; m_IsRefreshingFromScroll = true; diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs index 95d1f7a..9e99e8e 100644 --- a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -147,6 +147,22 @@ namespace XericUI.XTable.Rendering.Elements KillObject(instance); } + /// 销毁所有按 cellId 绑定的实例,并在销毁前通知调用方。 + public void ReleaseAll(System.Action beforeRelease = null) + { + foreach (var pool in m_Pools.Values) + { + foreach (var instance in pool.Slots.Values) + { + beforeRelease?.Invoke(instance); + KillObject(instance); + } + pool.Slots.Clear(); + } + m_Pools.Clear(); + m_TotalCreated = 0; + } + /// 结束当前渲染周期。实例保留在全局槽位中,以便跨区域复用。 public void ReturnAll() {