diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index 4cbaab2..b8e5350 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -90,22 +90,13 @@ namespace XericUI.XTable.Rendering.Component sc = Mathf.Max(sc, StartCol); er = Mathf.Min(er, EndRow > 0 ? EndRow : rows.Length); ec = Mathf.Min(ec, EndCol > 0 ? EndCol : cols.Length); - if (sr >= er || sc >= ec) - { - if (fullRefresh) ReleaseAll(); - return; - } + if (sr >= er || sc >= ec) return; var target = new HashSet<(int, int)>(); for (int r = sr; r < er; r++) for (int c = sc; c < ec; c++) target.Add((r, c)); - if (fullRefresh) - ReleaseAll(); - else - ReleaseExcept(target); - foreach (var cell in target) if (fullRefresh || !m_Rendered.Contains(cell)) RenderCell(cell.Item1, cell.Item2, rows, cols, editorMode); @@ -122,18 +113,6 @@ namespace XericUI.XTable.Rendering.Component m_Rendered.Clear(); } - private void ReleaseExcept(HashSet<(int, int)> target) - { - var remove = new List<(int, int)>(); - foreach (var pair in m_Tracked) - if (!target.Contains(pair.Key)) remove.Add(pair.Key); - foreach (var key in remove) - { - ReleaseTracked(m_Tracked[key]); - m_Tracked.Remove(key); - } - } - private void ReleaseTracked(Tracked tracked) { Assembler.ReleaseText(tracked.Text); @@ -191,15 +170,20 @@ namespace XericUI.XTable.Rendering.Component } else if (cell.ContentType == CellContentType.Prefab && cell.Prefab != null && PrefabPool != null) { - var prefab = PrefabPool.Acquire(cell.Prefab, cellId, m_Parent); + var key = (row, col); + GameObject activePrefab = null; + bool wasActive = ActivePrefabMap != null + && ActivePrefabMap.TryGetValue(key, out activePrefab); + var prefab = wasActive ? activePrefab : PrefabPool.Acquire(cell.Prefab, cellId, m_Parent); if (prefab != null) { var rt = prefab.GetComponent(); rt.anchoredPosition = new Vector2(x, y); rt.sizeDelta = new Vector2(width, height); if (ActivePrefabMap != null) - ActivePrefabMap[(row, col)] = prefab; - cell.OnPrefabAcquire?.Invoke(prefab); + ActivePrefabMap[key] = prefab; + if (!wasActive) + cell.OnPrefabAcquire?.Invoke(prefab); } } } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs index 64ddf34..51c3092 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Api.cs @@ -452,7 +452,9 @@ namespace XericUI.XTable.Rendering.Component public void RemoveRow(int row) { if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.RemoveRow, row))) return; + var prefabCallbacks = CaptureActivePrefabReleaseCallbacks(); RemoveRowImmediate(row); + RebuildRenderBindings(prefabCallbacks); } private void RemoveRowImmediate(int row) @@ -509,7 +511,9 @@ namespace XericUI.XTable.Rendering.Component public void RemoveColumn(int col) { if (TryEnqueueUpdateCommand(new UpdateCommand(UpdateCommandType.RemoveColumn, col: col))) return; + var prefabCallbacks = CaptureActivePrefabReleaseCallbacks(); RemoveColumnImmediate(col); + RebuildRenderBindings(prefabCallbacks); } private void RemoveColumnImmediate(int col) @@ -762,26 +766,16 @@ namespace XericUI.XTable.Rendering.Component public void ClearData() { ThrowIfCheckingUpdate(nameof(ClearData)); + var prefabCallbacks = CaptureActivePrefabReleaseCallbacks(); if (m_TableData != null) { - // 清理预制体:先调用 OnPrefabRelease 再清理映射 - if (m_ActivePrefabMap != null) - { - foreach (var kv in m_ActivePrefabMap) - { - var cellData = m_TableData.GetCell(kv.Key.Item1, kv.Key.Item2); - cellData?.OnPrefabRelease?.Invoke(kv.Value); - } - m_ActivePrefabMap.Clear(); - } - m_PrefabPool?.ReturnAll(); - m_TableData.ClearAllMerges(); m_TableData.BlockMap.Clear(); m_StyleNamespaceDirty = true; } m_Quadtree?.Clear(); ClearSelection(); + RebuildRenderBindings(prefabCallbacks); MarkDirty(TableDirtyType.DataChanged); RefreshView(); } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs index dba47b2..57e3aef 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs @@ -54,6 +54,7 @@ namespace XericUI.XTable.Rendering.Component { Assembler = m_Assembler, PrefabPool = m_PrefabPool, + ActivePrefabMap = m_ActivePrefabMap, CellPadding = CellPadding, Config = Config, SelectionTable = this, diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index 5c4733a..fc2aade 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -19,11 +19,12 @@ namespace XericUI.XTable.Rendering.Component /// 可复用列表,避免每帧分配 GC [NonSerialized] private List<(int, int)> m_ReusableCellList = new(capacity: 128); + [NonSerialized] private List m_ReusableQuadrants = new(capacity: 32); /// 控制滚动时是否使用四叉树(大表格推荐) private bool UseQuadtreeForScroll => m_Quadtree != null && m_Quadtree.IsBuilt - && m_RowHeights.Length * m_ColWidths.Length > 2000; + && m_RowHeights.Length * m_ColWidths.Length > 2000; private struct CellTrackedObjs { @@ -52,6 +53,7 @@ namespace XericUI.XTable.Rendering.Component m_Assembler?.ReleaseText(objs.CellText); m_Assembler?.ReleaseImage(objs.CellImage); } + m_CellTracked.Clear(); m_LastRenderedCells.Clear(); } @@ -212,13 +214,19 @@ namespace XericUI.XTable.Rendering.Component { if (data.Prefab == null) return; - GameObject instance = m_PrefabPool.Acquire(data.Prefab, cellId); + var key = (row, col); + GameObject instance = null; + bool wasActive = m_ActivePrefabMap != null + && m_ActivePrefabMap.TryGetValue(key, out instance); + if (!wasActive) + instance = m_PrefabPool.Acquire(data.Prefab, cellId); if (instance == null) return; RectTransform prt = instance.GetComponent(); prt.anchoredPosition = new Vector2(cellX, cellY); prt.sizeDelta = new Vector2(cellW, cellH); - m_ActivePrefabMap[(row, col)] = instance; - data.OnPrefabAcquire?.Invoke(instance); + m_ActivePrefabMap[key] = instance; + if (!wasActive) + data.OnPrefabAcquire?.Invoke(instance); } #endregion @@ -227,6 +235,8 @@ namespace XericUI.XTable.Rendering.Component private void ReleaseRenderedBindings(Dictionary> prefabReleaseCallbacks = null) { + // 结构重建必须解除所有 cellId 映射;不能只依赖当前可见区域的 tracked 集合。 + m_Assembler?.ReleaseAllBindings(); ReleaseAllTrackedCells(); ReleaseFrozenCells(); m_LastRenderedCells.Clear(); @@ -238,6 +248,12 @@ namespace XericUI.XTable.Rendering.Component m_ActivePrefabMap?.Clear(); } + /// 结构删除后的唯一绑定重建入口;普通刷新和滚动不得调用。 + private void RebuildRenderBindings(Dictionary> prefabReleaseCallbacks) + { + ReleaseRenderedBindings(prefabReleaseCallbacks); + } + public void RefreshView(bool releaseBindings = true) { TableDirtyType refreshedFlags = m_DirtyFlag.Flags; @@ -271,9 +287,6 @@ namespace XericUI.XTable.Rendering.Component UpdateBackground(); - if (releaseBindings) - ReleaseRenderedBindings(CaptureActivePrefabReleaseCallbacks()); - // 清理孤立子对象 CleanOrphanChildren(); @@ -334,32 +347,9 @@ namespace XericUI.XTable.Rendering.Component var newVisible = new HashSet<(int, int)>(); ComputeVisibleCellsScrollInto(newVisible); - // 2. 回收不再可见的追踪对象 - m_ReusableCellList.Clear(); - foreach (var cell in m_LastRenderedCells) - if (!newVisible.Contains(cell)) - m_ReusableCellList.Add(cell); - foreach (var cell in m_ReusableCellList) - ReleaseTrackedCell(cell.Item1, cell.Item2); + // 2. 离屏单元格保持其绑定,供后续重绘直接复用。 - // 3. 回收不再可见的预制体(仅从映射表移除,预制体保留在计数器槽位中复用) - if (m_ActivePrefabMap != null && m_ActivePrefabMap.Count > 0) - { - m_ReusableCellList.Clear(); - foreach (var kv in m_ActivePrefabMap) - if (!newVisible.Contains(kv.Key)) - m_ReusableCellList.Add(kv.Key); - - foreach (var key in m_ReusableCellList) - { - GameObject instance = m_ActivePrefabMap[key]; - var data = TableData.GetCell(key.Item1, key.Item2); - m_PrefabPool?.Release(instance, data?.OnPrefabRelease); - m_ActivePrefabMap.Remove(key); - } - } - - // 4. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的 + // 3. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的 foreach (var cell in newVisible) { if (!m_LastRenderedCells.Contains(cell)) @@ -411,7 +401,8 @@ namespace XericUI.XTable.Rendering.Component if (m_BackgroundRenderer != null) return; var backgroundGo = new GameObject("_TableBackground", - typeof(RectTransform), typeof(CanvasRenderer), typeof(XTableBackgroundRenderer), typeof(XTableSelectionInput)); + typeof(RectTransform), typeof(CanvasRenderer), typeof(XTableBackgroundRenderer), + typeof(XTableSelectionInput)); backgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; backgroundGo.transform.SetParent(ContentTransform, false); backgroundGo.transform.SetAsFirstSibling(); @@ -451,7 +442,8 @@ namespace XericUI.XTable.Rendering.Component { if (string.IsNullOrEmpty(quadrant.Namespace) || quadrant.Namespace == baseNamespace) continue; - rects.Add(new XTableBackgroundRenderer.RectData(quadrant.Region, GetCellBgColor(quadrant.Namespace))); + rects.Add(new XTableBackgroundRenderer.RectData(quadrant.Region, + GetCellBgColor(quadrant.Namespace))); } } } @@ -474,4 +466,4 @@ namespace XericUI.XTable.Rendering.Component #endregion } -} +} \ No newline at end of file diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs index 7e7dce6..944fef8 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs @@ -72,17 +72,20 @@ namespace XericUI.XTable.Rendering.Component private sealed class UpdateContext { public TableDirtyType DirtyFlags; + public bool RequiresBindingRebuild; public readonly List Commands = new(); public void Reset() { DirtyFlags = TableDirtyType.None; + RequiresBindingRebuild = false; Commands.Clear(); } public void MergeFrom(UpdateContext child) { DirtyFlags |= child.DirtyFlags; + RequiresBindingRebuild |= child.RequiresBindingRebuild; Commands.AddRange(child.Commands); } } @@ -134,7 +137,10 @@ namespace XericUI.XTable.Rendering.Component private bool TryEnqueueUpdateCommand(UpdateCommand command) { if (!IsCheckingUpdate) return false; - m_UpdateContextStack.Peek().Commands.Add(command); + UpdateContext context = m_UpdateContextStack.Peek(); + context.Commands.Add(command); + context.RequiresBindingRebuild |= command.Type == UpdateCommandType.RemoveRow + || command.Type == UpdateCommandType.RemoveColumn; return true; } @@ -158,12 +164,14 @@ namespace XericUI.XTable.Rendering.Component m_UpdateContextStack.Push(context); try { - Dictionary> prefabCallbacks = CaptureActivePrefabReleaseCallbacks(); + Dictionary> prefabCallbacks = context.RequiresBindingRebuild + ? CaptureActivePrefabReleaseCallbacks() : null; ExecuteUpdateCommands(context); if (context.DirtyFlags == TableDirtyType.None) return; m_UpdateContextStack.Pop(); - ReleaseRenderedBindings(prefabCallbacks); + if (context.RequiresBindingRebuild) + RebuildRenderBindings(prefabCallbacks); ApplyDirty(context.DirtyFlags); RefreshView(false); RefreshSelectionOverlays(); diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index 61079ca..969d797 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using TMPro; using UnityEngine; @@ -7,7 +8,7 @@ namespace XericUI.XTable.Rendering.Elements { /// /// 表格单元动态拼装器——通过 SlotCounterPool 管理 Image + TMP_Text 的组合创建与回收。 - /// 每个单元格通过 cellId 映射到固定索引的计数器槽位,空闲槽位经 FrozenSlots 全局流转复用。 + /// cellId 在绑定期间稳定复用同一对象;对象显隐仅随所属 Counter 整体借出或归还变更。 /// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。 /// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。 /// @@ -21,7 +22,6 @@ namespace XericUI.XTable.Rendering.Elements public RectTransform Rect; public Image Image; public StyleBoundElement Style; - /// 绑定的单元格 ID,用于 Release 定位槽位 public int CellId; } @@ -31,7 +31,6 @@ namespace XericUI.XTable.Rendering.Elements public RectTransform Rect; public TMP_Text Text; public StyleBoundElement Style; - /// 绑定的单元格 ID,用于 Release 定位槽位 public int CellId; } @@ -40,19 +39,10 @@ namespace XericUI.XTable.Rendering.Elements #region 字段 private Transform m_Parent; - - /// 运行时的默认样式命名空间(编辑器下为 null,不绑定) private string m_DefaultStyleNamespace; - - /// 所有已绑定的 StyleBoundElement 回调集合(用于样式变更时通知外部) public event System.Action OnStyleChangeDetected; - - /// 基于计数器的 Image 槽位对象池 private SlotCounterPool m_ImagePool; - /// 基于计数器的 Text 槽位对象池 private SlotCounterPool m_TextPool; - - /// HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示) private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy; #endregion @@ -63,11 +53,13 @@ namespace XericUI.XTable.Rendering.Elements { m_Parent = parent; m_DefaultStyleNamespace = defaultStyleNamespace; - m_ImagePool = new SlotCounterPool(() => CreateImage("cell_img")); - m_TextPool = new SlotCounterPool(() => CreateText("cell_text")); + m_ImagePool = new SlotCounterPool(() => CreateImage("cell_img"), + SetImageCounterActive, SetImageCounterInactive); + m_TextPool = new SlotCounterPool(() => CreateText("cell_text"), + SetTextCounterActive, SetTextCounterInactive); } - /// 彻底销毁所有池对象(包括计数器内活跃槽位和冻结槽位) + /// 彻底销毁所有池对象。 public void DestroyAll() { m_ImagePool.DestroyAll(item => KillObject(item.Root)); @@ -95,48 +87,57 @@ namespace XericUI.XTable.Rendering.Elements #region 池操作 - /// 为指定 cellId 获取一个池化 Image + /// 为指定 cellId 获取池化 Image;已有绑定原样返回。 public PooledImage GetImage(int cellId, string name = "cell_img", string styleNamespace = null, Transform parent = null) { - var item = m_ImagePool.Acquire(cellId); + var item = m_ImagePool.Acquire(cellId, out bool isNewBinding); if (item == null) return null; + if (!isNewBinding) + { + if (parent != null && item.Root.transform.parent != parent) + item.Root.transform.SetParent(parent, false); + BindStyle(item.Style, styleNamespace); + return item; + } if (parent != null && item.Root.transform.parent != parent) item.Root.transform.SetParent(parent, false); item.CellId = cellId; - item.Root.SetActive(true); item.Root.name = name; ResetImageDefaults(item); BindStyle(item.Style, styleNamespace); return item; } - /// 为指定 cellId 获取一个池化 TMP_Text + /// 为指定 cellId 获取池化 TMP_Text;已有绑定原样返回。 public PooledText GetText(int cellId, string name = "cell_text", string styleNamespace = null, Transform parent = null) { - var item = m_TextPool.Acquire(cellId); + var item = m_TextPool.Acquire(cellId, out bool isNewBinding); if (item == null) return null; + if (!isNewBinding) + { + if (parent != null && item.Root.transform.parent != parent) + item.Root.transform.SetParent(parent, false); + BindStyle(item.Style, styleNamespace); + return item; + } if (parent != null && item.Root.transform.parent != parent) item.Root.transform.SetParent(parent, false); item.CellId = cellId; - item.Root.SetActive(true); item.Root.name = name; ResetTextDefaults(item); BindStyle(item.Style, styleNamespace); return item; } - /// 运行时绑定样式到池元素的 StyleBoundElement,实现被动更新(push) private void BindStyle(StyleBoundElement style, string cellNamespace) { if (!Application.isPlaying) return; - string ns = cellNamespace ?? m_DefaultStyleNamespace; if (string.IsNullOrEmpty(ns)) return; - if (style != null && style.BoundNamespace != ns) { style.OnStyleChangedCallback -= OnStyleChangedNotify; @@ -150,39 +151,64 @@ namespace XericUI.XTable.Rendering.Elements OnStyleChangeDetected?.Invoke(); } - /// 归还所有槽位(全量刷新时调用)。被归还对象会隐藏,直到下次 Acquire。 + /// 解除指定 cellId 的 Image 与 Text 槽位绑定。 + public void ReleaseCell(int cellId) + { + m_ImagePool.ReleaseCell(cellId); + m_TextPool.ReleaseCell(cellId); + } + + /// 解除全部 Image 与 Text 槽位绑定。 + public void ReleaseAllBindings() + { + m_ImagePool.ReleaseAllBindings(); + m_TextPool.ReleaseAllBindings(); + } + + /// 兼容旧 API:解除全部槽位绑定。 public void ReturnAll() { - m_ImagePool.ReturnAll(HideImage); - m_TextPool.ReturnAll(HideText); + ReleaseAllBindings(); } - /// 归还单个 Image 池对象并隐藏。 + /// 解除指定 Image 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。 public void ReleaseImage(PooledImage item) { - if (item == null) return; - HideImage(item); - m_ImagePool.ReleaseByObject(item); + if (item != null) m_ImagePool.ReleaseCell(item.CellId); } - /// 归还单个 Text 池对象并隐藏。 + /// 解除指定 Text 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。 public void ReleaseText(PooledText item) { - if (item == null) return; - HideText(item); - m_TextPool.ReleaseByObject(item); + if (item != null) m_TextPool.ReleaseCell(item.CellId); } - private static void HideImage(PooledImage item) + private static void SetImageCounterActive(IReadOnlyList items) { - item.Style?.Unbind(); - item.Root.SetActive(false); + for (int i = 0; i < items.Count; i++) items[i].Root.SetActive(true); } - private static void HideText(PooledText item) + private static void SetImageCounterInactive(IReadOnlyList items) { - item.Style?.Unbind(); - item.Root.SetActive(false); + for (int i = 0; i < items.Count; i++) + { + items[i].Style?.Unbind(); + items[i].Root.SetActive(false); + } + } + + private static void SetTextCounterActive(IReadOnlyList items) + { + for (int i = 0; i < items.Count; i++) items[i].Root.SetActive(true); + } + + private static void SetTextCounterInactive(IReadOnlyList items) + { + for (int i = 0; i < items.Count; i++) + { + items[i].Style?.Unbind(); + items[i].Root.SetActive(false); + } } #endregion @@ -228,19 +254,11 @@ namespace XericUI.XTable.Rendering.Elements Image img = go.GetComponent(); img.raycastTarget = false; - RectTransform rect = go.GetComponent(); rect.anchorMin = ANCHOR_TOPLEFT_MIN; rect.anchorMax = ANCHOR_TOPLEFT_MAX; rect.pivot = PIVOT_TOPLEFT; - - return new PooledImage - { - Root = go, - Rect = rect, - Image = img, - Style = new StyleBoundElement { ImageComponent = img } - }; + return new PooledImage { Root = go, Rect = rect, Image = img, Style = new StyleBoundElement { ImageComponent = img } }; } private PooledText CreateText(string name) @@ -254,19 +272,11 @@ namespace XericUI.XTable.Rendering.Elements text.raycastTarget = false; text.alignment = TextAlignmentOptions.Midline; text.fontSize = 14; - RectTransform rect = go.GetComponent(); rect.anchorMin = ANCHOR_TOPLEFT_MIN; rect.anchorMax = ANCHOR_TOPLEFT_MAX; rect.pivot = PIVOT_TOPLEFT; - - return new PooledText - { - Root = go, - Rect = rect, - Text = text, - Style = new StyleBoundElement { TextComponent = text } - }; + return new PooledText { Root = go, Rect = rect, Text = text, Style = new StyleBoundElement { TextComponent = text } }; } #endregion diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs index 62b8519..8b853be 100644 --- a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -30,14 +30,14 @@ namespace XericUI.XTable.Rendering.Elements Transform targetParent = parent ?? m_Parent; if (pool.ActiveSlots.TryGetValue(cellId, out GameObject existing)) { - Prepare(existing, targetParent, cellId); + UpdateLayout(existing, targetParent, cellId); return existing; } GameObject instance = pool.Standby.Count > 0 ? pool.Standby.Pop() : Create(prefab, targetParent); if (instance == null) return null; pool.ActiveSlots[cellId] = instance; - Prepare(instance, targetParent, cellId); + PrepareNewBinding(instance, targetParent, cellId); return instance; } @@ -111,7 +111,13 @@ namespace XericUI.XTable.Rendering.Elements return instance; } - private static void Prepare(GameObject instance, Transform parent, int cellId) + private static void PrepareNewBinding(GameObject instance, Transform parent, int cellId) + { + UpdateLayout(instance, parent, cellId); + instance.SetActive(true); + } + + private static void UpdateLayout(GameObject instance, Transform parent, int cellId) { if (parent != null && instance.transform.parent != parent) instance.transform.SetParent(parent, false); @@ -120,7 +126,6 @@ namespace XericUI.XTable.Rendering.Elements rt.anchorMin = AnchorTopLeft; rt.anchorMax = AnchorTopLeft; rt.pivot = PivotTopLeft; - instance.SetActive(true); } private static void KillObject(GameObject go) diff --git a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs index a8c2dce..1c32794 100644 --- a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs +++ b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs @@ -7,8 +7,7 @@ namespace XericUI.XTable.Rendering.Elements { /// /// 基于计数器的槽位对象池(泛型)。 - /// 每个 cellId 映射到固定索引的 SlotCounter,释放槽位保留在原计数器内复用。 - /// 避免跨计数器拆分碰撞组导致的对象位置冲突。 + /// cellId 在绑定期间稳定地映射到同一槽位;计数器仅在全部槽位解除绑定后整体待命复用。 /// /// 池对象类型 public class SlotCounterPool where T : class @@ -17,32 +16,30 @@ namespace XericUI.XTable.Rendering.Elements private class Slot { - /// 当前绑定的单元格 ID(-1 = 空闲,可被偷取) public int CellId = -1; - /// 池对象引用 public T PooledObject; } private class SlotCounter { - public List Slots = new List(capacity: 4); + public readonly List Slots = new List(capacity: 4); + public int BoundSlotCount; } #endregion #region 字段 - /// 计数器数组大小(= 池上限,也是 cellId 取模的模数) + /// 计数器数组大小(= 池上限,也是 cellId 路由键的模数) public const int POOL_SIZE = 256; - private SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE]; - - /// 已创建对象总数(不含预制体 Instantiate) + private readonly SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE]; + private readonly Stack m_StandbyCounters = new Stack(); + private readonly Func m_Factory; + private readonly Action> m_OnCounterBorrowed; + private readonly Action> m_OnCounterReturned; private int m_CreatedCount; - /// 对象工厂(构造函数传入) - private Func m_Factory; - /// 已创建对象总数(只读) public int CreatedCount => m_CreatedCount; @@ -50,106 +47,116 @@ namespace XericUI.XTable.Rendering.Elements #region 构造 - public SlotCounterPool(Func factory) + public SlotCounterPool(Func factory, Action> onCounterBorrowed = null, + Action> onCounterReturned = null) { m_Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + m_OnCounterBorrowed = onCounterBorrowed; + m_OnCounterReturned = onCounterReturned; } #endregion #region 池操作 - /// - /// 为指定 cellId 获取一个池对象。 - /// 优先复用本计数器内的空闲槽位,最后创建新对象。 - /// - public T Acquire(int cellId) + /// 为指定 cellId 获取池对象,并指示是否建立了新槽位绑定。 + public T Acquire(int cellId, out bool isNewBinding) { - int index = cellId % POOL_SIZE; - if (index < 0) index += POOL_SIZE; // 处理负 cellId - + int index = GetRouteIndex(cellId); var counter = m_Counters[index]; + bool borrowedCounter = false; if (counter == null) { - counter = new SlotCounter(); + counter = m_StandbyCounters.Count > 0 ? m_StandbyCounters.Pop() : new SlotCounter(); m_Counters[index] = counter; + borrowedCounter = true; } - // 1. 在计数器内查找空闲槽位 + for (int i = 0; i < counter.Slots.Count; i++) + { + var slot = counter.Slots[i]; + if (slot.CellId == cellId) + { + isNewBinding = false; + return slot.PooledObject; + } + } + + Slot availableSlot = null; for (int i = 0; i < counter.Slots.Count; i++) { if (counter.Slots[i].CellId == -1) { - counter.Slots[i].CellId = cellId; - return counter.Slots[i].PooledObject; + availableSlot = counter.Slots[i]; + break; } } - // 2. 创建新对象。槽位不能跨计数器流转,避免破坏碰撞单元格的稳定关系。 - if (m_CreatedCount < POOL_SIZE) + if (availableSlot == null) { - var obj = m_Factory(); - var slot = new Slot { CellId = cellId, PooledObject = obj }; - counter.Slots.Add(slot); + if (m_CreatedCount >= POOL_SIZE) + { + if (borrowedCounter) + { + m_Counters[index] = null; + m_StandbyCounters.Push(counter); + } + Debug.LogWarning($"[SlotCounterPool<{typeof(T).Name}>] 已达上限 {POOL_SIZE},无法分配新对象"); + isNewBinding = false; + return null; + } + + availableSlot = new Slot { PooledObject = m_Factory() }; + counter.Slots.Add(availableSlot); m_CreatedCount++; - return obj; } - // 4. 已达上限 - Debug.LogWarning($"[SlotCounterPool<{typeof(T).Name}>] 已达上限 {POOL_SIZE},无法分配新对象"); - return null; + availableSlot.CellId = cellId; + counter.BoundSlotCount++; + isNewBinding = true; + if (borrowedCounter) + m_OnCounterBorrowed?.Invoke(GetObjects(counter)); + return availableSlot.PooledObject; } - /// - /// 释放指定 cellId 占用的槽位,并保留在原计数器中供相同碰撞组复用。 - /// - public void Release(int cellId) + /// 为指定 cellId 获取一个池对象。 + public T Acquire(int cellId) { - int index = cellId % POOL_SIZE; - if (index < 0) index += POOL_SIZE; + return Acquire(cellId, out _); + } + /// 解除 cellId 的槽位绑定;仅计数器完全空闲时才将其整体归还待命栈。 + public bool ReleaseCell(int cellId) + { + int index = GetRouteIndex(cellId); var counter = m_Counters[index]; - if (counter == null) return; + if (counter == null) return false; for (int i = 0; i < counter.Slots.Count; i++) { - if (counter.Slots[i].CellId == cellId) - { - counter.Slots[i].CellId = -1; - return; - } - } - } + var slot = counter.Slots[i]; + if (slot.CellId != cellId) continue; - /// - /// 尝试通过池对象引用查找并释放对应槽位。 - /// 用于 Release(PooledObject) 场景,需要遍历查找。 - /// - public bool ReleaseByObject(T pooledObject) - { - if (pooledObject == null) return false; - - // 先遍历所有计数器的活跃槽位 - for (int ci = 0; ci < POOL_SIZE; ci++) - { - var counter = m_Counters[ci]; - if (counter == null) continue; - - for (int i = 0; i < counter.Slots.Count; i++) - { - if (ReferenceEquals(counter.Slots[i].PooledObject, pooledObject)) - { - counter.Slots[i].CellId = -1; - return true; - } - } + slot.CellId = -1; + counter.BoundSlotCount--; + if (counter.BoundSlotCount == 0) + ReturnCounter(index, counter); + return true; } return false; } - /// 释放所有活跃槽位;回调在槽位变为空闲前执行。 - public void ReturnAll(Action beforeRelease = null) + /// 兼容旧 API:解除指定 cellId 的绑定。 + public void Release(int cellId) { + ReleaseCell(cellId); + } + + /// 通过池对象引用解除对应 cellId 的槽位绑定。 + public bool ReleaseByObject(T pooledObject) + { + if (pooledObject == null) return false; + for (int ci = 0; ci < POOL_SIZE; ci++) { var counter = m_Counters[ci]; @@ -157,33 +164,81 @@ namespace XericUI.XTable.Rendering.Elements for (int i = 0; i < counter.Slots.Count; i++) { var slot = counter.Slots[i]; - if (slot.CellId == -1) continue; - beforeRelease?.Invoke(slot.PooledObject); - slot.CellId = -1; + if (slot.CellId != -1 && ReferenceEquals(slot.PooledObject, pooledObject)) + return ReleaseCell(slot.CellId); } } + return false; } - /// - /// 彻底销毁所有池对象。 - /// - public void DestroyAll(Action destroyFunc) + /// 解除全部槽位绑定,并将所有计数器整体归还待命栈。 + public void ReleaseAllBindings() { - if (destroyFunc == null) return; - for (int ci = 0; ci < POOL_SIZE; ci++) { var counter = m_Counters[ci]; if (counter == null) continue; - - foreach (var slot in counter.Slots) - destroyFunc(slot.PooledObject); - counter.Slots.Clear(); + for (int i = 0; i < counter.Slots.Count; i++) + counter.Slots[i].CellId = -1; + counter.BoundSlotCount = 0; + ReturnCounter(ci, counter); } + } + /// 兼容旧 API:解除所有活跃槽位绑定。 + public void ReturnAll(Action beforeRelease = null) + { + if (beforeRelease != null) + { + for (int ci = 0; ci < POOL_SIZE; ci++) + { + var counter = m_Counters[ci]; + if (counter == null) continue; + for (int i = 0; i < counter.Slots.Count; i++) + if (counter.Slots[i].CellId != -1) beforeRelease(counter.Slots[i].PooledObject); + } + } + ReleaseAllBindings(); + } + + /// 彻底销毁所有池对象。 + public void DestroyAll(Action destroyFunc) + { + if (destroyFunc == null) return; + var destroyedCounters = new HashSet(); + for (int ci = 0; ci < POOL_SIZE; ci++) + { + var counter = m_Counters[ci]; + if (counter != null) destroyedCounters.Add(counter); + m_Counters[ci] = null; + } + foreach (var counter in m_StandbyCounters) destroyedCounters.Add(counter); + m_StandbyCounters.Clear(); + foreach (var counter in destroyedCounters) + foreach (var slot in counter.Slots) destroyFunc(slot.PooledObject); m_CreatedCount = 0; } + private static int GetRouteIndex(int cellId) + { + int index = cellId % POOL_SIZE; + return index < 0 ? index + POOL_SIZE : index; + } + + private void ReturnCounter(int index, SlotCounter counter) + { + m_OnCounterReturned?.Invoke(GetObjects(counter)); + m_Counters[index] = null; + m_StandbyCounters.Push(counter); + } + + private static IReadOnlyList GetObjects(SlotCounter counter) + { + var objects = new List(counter.Slots.Count); + for (int i = 0; i < counter.Slots.Count; i++) objects.Add(counter.Slots[i].PooledObject); + return objects; + } + #endregion } }