From b132e8e167e9c39a2df518c186187b22d2ceb966 Mon Sep 17 00:00:00 2001 From: lrc <571244399@qq.com> Date: Tue, 18 Aug 2026 13:43:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=A1=A8=E6=A0=BC=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=AF=B9=E8=B1=A1=E5=A4=8D=E7=94=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/XTable/Core/XTableConfig.cs | 8 ++ .../Rendering/Component/TableRenderer.cs | 59 +++++++++++--- .../Component/XericUIActionTable.Freeze.cs | 1 + .../XericUIActionTable.RendererFilter.cs | 78 +++++++++++++------ .../XericUIActionTable.UpdateScope.cs | 1 + .../Rendering/Component/XericUIActionTable.cs | 3 +- .../Rendering/Elements/CellAssembler.cs | 10 +-- .../Rendering/Elements/PrefabPrototypePool.cs | 7 ++ .../Rendering/Elements/SlotCounterPool.cs | 36 ++++++--- 9 files changed, 155 insertions(+), 48 deletions(-) diff --git a/Runtime/XTable/Core/XTableConfig.cs b/Runtime/XTable/Core/XTableConfig.cs index fbd040b..dd53e4f 100644 --- a/Runtime/XTable/Core/XTableConfig.cs +++ b/Runtime/XTable/Core/XTableConfig.cs @@ -47,6 +47,14 @@ namespace XericUI.XTable.Core [Tooltip("预制体池允许创建的实例总上限")] public int PrefabPoolMaxCapacity = 256; + [Min(1)] + [Tooltip("文本池 cellId 路由桶数量")] + public int TextPoolRouteBucketCount = 256; + + [Min(1)] + [Tooltip("图片池 cellId 路由桶数量")] + public int ImagePoolRouteBucketCount = 256; + #endregion #region 样式回退值 diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index b8e5350..954c5f1 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -19,6 +19,7 @@ namespace XericUI.XTable.Rendering.Component private readonly Transform m_Parent; private readonly Dictionary<(int, int), Tracked> m_Tracked = new(); private readonly HashSet<(int, int)> m_Rendered = new(); + private readonly HashSet<(int, int)> m_Target = new(); private readonly Dictionary m_SpriteCache = new(); private GameObject m_BackgroundGo; @@ -90,27 +91,51 @@ 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) return; - var target = new HashSet<(int, int)>(); + m_Target.Clear(); + if (sr >= er || sc >= ec) + { + ReleaseAll(); + return; + } for (int r = sr; r < er; r++) for (int c = sc; c < ec; c++) - target.Add((r, c)); + m_Target.Add((r, c)); - foreach (var cell in target) + foreach (var cell in m_Rendered) + if (!m_Target.Contains(cell)) + ReleaseCell(cell.Item1, cell.Item2); + + foreach (var cell in m_Target) if (fullRefresh || !m_Rendered.Contains(cell)) RenderCell(cell.Item1, cell.Item2, rows, cols, editorMode); m_Rendered.Clear(); - foreach (var cell in target) m_Rendered.Add(cell); + foreach (var cell in m_Target) m_Rendered.Add(cell); } public void ReleaseAll() { - foreach (var pair in m_Tracked) - ReleaseTracked(pair.Value); + foreach (var cell in m_Rendered) + ReleaseCell(cell.Item1, cell.Item2); m_Tracked.Clear(); m_Rendered.Clear(); + m_Target.Clear(); + } + + private void ReleaseCell(int row, int col) + { + var key = (row, col); + if (m_Tracked.TryGetValue(key, out var tracked)) + { + ReleaseTracked(tracked); + m_Tracked.Remove(key); + } + if (ActivePrefabMap != null && ActivePrefabMap.TryGetValue(key, out var instance)) + { + PrefabPool?.Release(instance, GetTableData?.Invoke()?.GetCell(row, col)?.OnPrefabRelease); + ActivePrefabMap.Remove(key); + } } private void ReleaseTracked(Tracked tracked) @@ -136,6 +161,8 @@ namespace XericUI.XTable.Rendering.Component } int cellId = row * cols.Length + col; + var key = (row, col); + m_Tracked.TryGetValue(key, out var previousTracked); var tracked = new Tracked(); var cell = data?.GetCell(row, col); if (cell != null) @@ -170,10 +197,16 @@ namespace XericUI.XTable.Rendering.Component } else if (cell.ContentType == CellContentType.Prefab && cell.Prefab != null && PrefabPool != null) { - var key = (row, col); + key = (row, col); GameObject activePrefab = null; bool wasActive = ActivePrefabMap != null && ActivePrefabMap.TryGetValue(key, out activePrefab); + if (wasActive && !PrefabPool.IsInstanceOfPrefab(activePrefab, cell.Prefab)) + { + PrefabPool.Release(activePrefab, cell.OnPrefabRelease); + ActivePrefabMap.Remove(key); + wasActive = false; + } var prefab = wasActive ? activePrefab : PrefabPool.Acquire(cell.Prefab, cellId, m_Parent); if (prefab != null) { @@ -187,7 +220,15 @@ namespace XericUI.XTable.Rendering.Component } } } - m_Tracked[(row, col)] = tracked; + if (previousTracked.Text != null && previousTracked.Text != tracked.Text) + Assembler.ReleaseText(previousTracked.Text); + if (previousTracked.Image != null && previousTracked.Image != tracked.Image) + Assembler.ReleaseImage(previousTracked.Image); + + if (tracked.Text != null || tracked.Image != null) + m_Tracked[key] = tracked; + else + m_Tracked.Remove(key); } private Sprite GetSprite(Texture2D texture) diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs index f17ebc8..524c571 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.Freeze.cs @@ -1,5 +1,6 @@ using UnityEngine; using XericUI.XTable.Rendering.Elements; +using XericLibrary.Runtime.MacroLibrary; namespace XericUI.XTable.Rendering.Component { diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index ab7e82e..b8cf8f6 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -16,6 +16,7 @@ namespace XericUI.XTable.Rendering.Component #region 增量渲染追踪 [NonSerialized] private HashSet<(int, int)> m_LastRenderedCells = new(); + [NonSerialized] private HashSet<(int, int)> m_VisibleCellsScratch = new(); [NonSerialized] private Dictionary<(int, int), CellTrackedObjs> m_CellTracked = new(); /// 可复用列表,避免每帧分配 GC @@ -40,10 +41,21 @@ namespace XericUI.XTable.Rendering.Component private void ReleaseTrackedCell(int row, int col) { var key = (row, col); - if (!m_CellTracked.TryGetValue(key, out var objs)) return; - m_Assembler?.ReleaseText(objs.CellText); - m_Assembler?.ReleaseImage(objs.CellImage); - m_CellTracked.Remove(key); + if (m_CellTracked.TryGetValue(key, out var objs)) + { + m_Assembler?.ReleaseText(objs.CellText); + m_Assembler?.ReleaseImage(objs.CellImage); + m_CellTracked.Remove(key); + } + ReleasePrefabCell(row, col); + } + + private void ReleasePrefabCell(int row, int col) + { + var key = (row, col); + if (m_ActivePrefabMap == null || !m_ActivePrefabMap.TryGetValue(key, out var instance)) return; + m_PrefabPool?.Release(instance, TableData.GetCell(row, col)?.OnPrefabRelease); + m_ActivePrefabMap.Remove(key); } private void ReleaseAllTrackedCells() @@ -148,6 +160,8 @@ namespace XericUI.XTable.Rendering.Component } int cellId = row * m_ColWidths.Length + col; + var key = (row, col); + m_CellTracked.TryGetValue(key, out var previousObjs); var objs = new CellTrackedObjs(); // ── 单元格内容 ── @@ -167,8 +181,16 @@ namespace XericUI.XTable.Rendering.Component } } - m_CellTracked[(row, col)] = objs; - m_LastRenderedCells.Add((row, col)); + if (previousObjs.CellText != null && previousObjs.CellText != objs.CellText) + m_Assembler.ReleaseText(previousObjs.CellText); + if (previousObjs.CellImage != null && previousObjs.CellImage != objs.CellImage) + m_Assembler.ReleaseImage(previousObjs.CellImage); + + if (objs.CellText != null || objs.CellImage != null) + m_CellTracked[key] = objs; + else + m_CellTracked.Remove(key); + m_LastRenderedCells.Add(key); } private void RenderCellText(int cellId, XTableCellData data, @@ -219,6 +241,12 @@ namespace XericUI.XTable.Rendering.Component GameObject instance = null; bool wasActive = m_ActivePrefabMap != null && m_ActivePrefabMap.TryGetValue(key, out instance); + if (wasActive && !m_PrefabPool.IsInstanceOfPrefab(instance, data.Prefab)) + { + m_PrefabPool.Release(instance, TableData.GetCell(row, col)?.OnPrefabRelease); + m_ActivePrefabMap.Remove(key); + wasActive = false; + } if (!wasActive) instance = m_PrefabPool.Acquire(data.Prefab, cellId); if (instance == null) return; @@ -262,7 +290,8 @@ namespace XericUI.XTable.Rendering.Component { if (rectTransform != null) m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace, - Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity); + Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity, + Config.TextPoolRouteBucketCount, Config.ImagePoolRouteBucketCount); else return; } @@ -293,16 +322,20 @@ namespace XericUI.XTable.Rendering.Component // 清理孤立子对象 CleanOrphanChildren(); - // 渲染所有可见单元格 + // 同步主区可见集合:只释放不再可见的绑定,保留仍可见单元格的路由归属。 #if UNITY_EDITOR bool editorMode = !Application.isPlaying; #else bool editorMode = false; #endif string ns = m_DefaultStyleNamespace; - ComputeVisibleCellsInto(m_LastRenderedCells); - foreach (var (r, c) in m_LastRenderedCells) - RenderCellContent(r, c, ns, editorMode); + ComputeVisibleCellsInto(m_VisibleCellsScratch); + foreach (var cell in m_LastRenderedCells) + if (!m_VisibleCellsScratch.Contains(cell)) + ReleaseTrackedCell(cell.Item1, cell.Item2); + foreach (var cell in m_VisibleCellsScratch) + RenderCellContent(cell.Item1, cell.Item2, ns, editorMode); + (m_LastRenderedCells, m_VisibleCellsScratch) = (m_VisibleCellsScratch, m_LastRenderedCells); // 主 Content 已先渲染,冻结区随后复用其释放的对象;边框按区域最后提交。 RenderFrozenCells(true); @@ -324,7 +357,8 @@ namespace XericUI.XTable.Rendering.Component { if (rectTransform != null) m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace, - Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity); + Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity, + Config.TextPoolRouteBucketCount, Config.ImagePoolRouteBucketCount); else return; } @@ -347,24 +381,24 @@ namespace XericUI.XTable.Rendering.Component // 选区由固定 Overlay 绘制,选择变化无需回收或重绘单元格内容。 - // 1. 计算新一帧的可见单元格(滚动路径用简单范围遍历,避免四叉树/分配开销) - var newVisible = new HashSet<(int, int)>(); - ComputeVisibleCellsScrollInto(newVisible); + // 1. 计算新一帧的可见单元格(滚动路径用简单范围遍历)。 + ComputeVisibleCellsScrollInto(m_VisibleCellsScratch); - // 2. 离屏单元格保持其绑定,供后续重绘直接复用。 + // 2. 先释放离屏单元格的 tracked 槽位与预制体实例;Counter 仅在全部槽位释放后整体归还。 + foreach (var cell in m_LastRenderedCells) + if (!m_VisibleCellsScratch.Contains(cell)) + ReleaseTrackedCell(cell.Item1, cell.Item2); - // 3. 渲染单元格:选区变更时全量重渲所有可见单元格,否则仅新进入的 - foreach (var cell in newVisible) - { + // 3. 仅渲染新进入的单元格。 + foreach (var cell in m_VisibleCellsScratch) if (!m_LastRenderedCells.Contains(cell)) RenderCellContent(cell.Item1, cell.Item2, ns, editorMode); - } // 冻结列、冻结行与左上角在 Content 后更新,并复用同一全局对象池。 RenderFrozenCells(false); - // 5. 交换:将 newVisible 作为下一帧的比较基准 - (m_LastRenderedCells, newVisible) = (newVisible, m_LastRenderedCells); + // 5. 交换:scratch 不作为 Compute 输出前保留旧集合。 + (m_LastRenderedCells, m_VisibleCellsScratch) = (m_VisibleCellsScratch, m_LastRenderedCells); m_DirtyFlag.ClearFlags(refreshedFlags & (TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)); OnTableRefreshed?.Invoke(); diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs index 944fef8..45435d2 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.UpdateScope.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using UnityEngine; using XericUI.XTable.Core; +using XericLibrary.Runtime.MacroLibrary; namespace XericUI.XTable.Rendering.Component { diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index f7a33a1..3608d82 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -221,7 +221,8 @@ namespace XericUI.XTable.Rendering.Component private void CreatePools() { m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace, - Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity); + Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity, + Config.TextPoolRouteBucketCount, Config.ImagePoolRouteBucketCount); m_PrefabPool = new PrefabPrototypePool(ContentTransform, Config.PrefabPoolMaxCapacity); } diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index bb89f29..7a2db73 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -50,14 +50,14 @@ namespace XericUI.XTable.Rendering.Elements #region 构造函数与析构 public CellAssembler(Transform parent, string defaultStyleNamespace = null, int textPoolMaxCapacity = 256, - int imagePoolMaxCapacity = 256) + int imagePoolMaxCapacity = 256, int textPoolRouteBucketCount = 256, int imagePoolRouteBucketCount = 256) { m_Parent = parent; m_DefaultStyleNamespace = defaultStyleNamespace; m_ImagePool = new SlotCounterPool(() => CreateImage("cell_img"), imagePoolMaxCapacity, - SetImageCounterActive, SetImageCounterInactive); + SetImageCounterActive, SetImageCounterInactive, imagePoolRouteBucketCount); m_TextPool = new SlotCounterPool(() => CreateText("cell_text"), textPoolMaxCapacity, - SetTextCounterActive, SetTextCounterInactive); + SetTextCounterActive, SetTextCounterInactive, textPoolRouteBucketCount); } /// 彻底销毁所有池对象。 @@ -175,13 +175,13 @@ namespace XericUI.XTable.Rendering.Elements /// 解除指定 Image 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。 public void ReleaseImage(PooledImage item) { - if (item != null) m_ImagePool.ReleaseCell(item.CellId); + if (item != null) m_ImagePool.ReleaseByObject(item); } /// 解除指定 Text 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。 public void ReleaseText(PooledText item) { - if (item != null) m_TextPool.ReleaseCell(item.CellId); + if (item != null) m_TextPool.ReleaseByObject(item); } private void SetImageCounterActive(IReadOnlyList items) diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs index 946a02b..093e760 100644 --- a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -59,6 +59,13 @@ namespace XericUI.XTable.Rendering.Elements } } + public bool IsInstanceOfPrefab(GameObject instance, GameObject prefab) + { + if (instance == null || prefab == null) return false; + return m_Pools.TryGetValue(prefab, out PrefabPool pool) + && pool.ActiveSlots.ContainsValue(instance); + } + public bool Release(GameObject instance, Action beforeRelease = null) { if (instance == null) return false; diff --git a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs index 5b4687b..b28a0a3 100644 --- a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs +++ b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs @@ -30,10 +30,9 @@ namespace XericUI.XTable.Rendering.Elements #region 字段 - /// cellId 路由桶数量。 - private const int ROUTE_BUCKET_COUNT = 256; + private const int DEFAULT_ROUTE_BUCKET_COUNT = 256; - private readonly SlotCounter[] m_Counters = new SlotCounter[ROUTE_BUCKET_COUNT]; + private readonly SlotCounter[] m_Counters; private readonly int m_MaxCreatedCount; private readonly Stack m_StandbyCounters = new Stack(); private readonly Func m_Factory; @@ -43,16 +42,31 @@ namespace XericUI.XTable.Rendering.Elements /// 已创建对象总数(只读) public int CreatedCount => m_CreatedCount; + public int MaxCreatedCount => m_MaxCreatedCount; + public int RouteBucketCount => m_Counters.Length; + public int ActiveCounterCount + { + get + { + int count = 0; + for (int i = 0; i < m_Counters.Length; i++) + if (m_Counters[i] != null) count++; + return count; + } + } + public bool IsPoolFull => m_CreatedCount >= m_MaxCreatedCount; #endregion #region 构造 public SlotCounterPool(Func factory, int maxCreatedCount = 256, - Action> onCounterBorrowed = null, Action> onCounterReturned = null) + Action> onCounterBorrowed = null, Action> onCounterReturned = null, + int routeBucketCount = DEFAULT_ROUTE_BUCKET_COUNT) { m_Factory = factory ?? throw new ArgumentNullException(nameof(factory)); m_MaxCreatedCount = Mathf.Max(1, maxCreatedCount); + m_Counters = new SlotCounter[Mathf.Max(1, routeBucketCount)]; m_OnCounterBorrowed = onCounterBorrowed; m_OnCounterReturned = onCounterReturned; } @@ -159,7 +173,7 @@ namespace XericUI.XTable.Rendering.Elements { if (pooledObject == null) return false; - for (int ci = 0; ci < ROUTE_BUCKET_COUNT; ci++) + for (int ci = 0; ci < m_Counters.Length; ci++) { var counter = m_Counters[ci]; if (counter == null) continue; @@ -176,7 +190,7 @@ namespace XericUI.XTable.Rendering.Elements /// 解除全部槽位绑定,并将所有计数器整体归还待命栈。 public void ReleaseAllBindings() { - for (int ci = 0; ci < ROUTE_BUCKET_COUNT; ci++) + for (int ci = 0; ci < m_Counters.Length; ci++) { var counter = m_Counters[ci]; if (counter == null) continue; @@ -192,7 +206,7 @@ namespace XericUI.XTable.Rendering.Elements { if (beforeRelease != null) { - for (int ci = 0; ci < ROUTE_BUCKET_COUNT; ci++) + for (int ci = 0; ci < m_Counters.Length; ci++) { var counter = m_Counters[ci]; if (counter == null) continue; @@ -208,7 +222,7 @@ namespace XericUI.XTable.Rendering.Elements { if (destroyFunc == null) return; var destroyedCounters = new HashSet(); - for (int ci = 0; ci < ROUTE_BUCKET_COUNT; ci++) + for (int ci = 0; ci < m_Counters.Length; ci++) { var counter = m_Counters[ci]; if (counter != null) destroyedCounters.Add(counter); @@ -221,10 +235,10 @@ namespace XericUI.XTable.Rendering.Elements m_CreatedCount = 0; } - private static int GetRouteIndex(int cellId) + private int GetRouteIndex(int cellId) { - int index = cellId % ROUTE_BUCKET_COUNT; - return index < 0 ? index + ROUTE_BUCKET_COUNT : index; + int index = cellId % m_Counters.Length; + return index < 0 ? index + m_Counters.Length : index; } private void ReturnCounter(int index, SlotCounter counter)