1 Commits
Author SHA1 Message Date
lrss3 b132e8e167 调整表格渲染对象复用逻辑 2026-08-18 13:43:49 +08:00
9 changed files with 155 additions and 48 deletions
+8
View File
@@ -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 退
@@ -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<Texture2D, Sprite> 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)
@@ -1,5 +1,6 @@
using UnityEngine;
using XericUI.XTable.Rendering.Elements;
using XericLibrary.Runtime.MacroLibrary;
namespace XericUI.XTable.Rendering.Component
{
@@ -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();
/// <summary>可复用列表,避免每帧分配 GC</summary>
@@ -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();
@@ -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
{
@@ -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);
}
@@ -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<PooledImage>(() => CreateImage("cell_img"), imagePoolMaxCapacity,
SetImageCounterActive, SetImageCounterInactive);
SetImageCounterActive, SetImageCounterInactive, imagePoolRouteBucketCount);
m_TextPool = new SlotCounterPool<PooledText>(() => CreateText("cell_text"), textPoolMaxCapacity,
SetTextCounterActive, SetTextCounterInactive);
SetTextCounterActive, SetTextCounterInactive, textPoolRouteBucketCount);
}
/// <summary>彻底销毁所有池对象。</summary>
@@ -175,13 +175,13 @@ namespace XericUI.XTable.Rendering.Elements
/// <summary>解除指定 Image 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。</summary>
public void ReleaseImage(PooledImage item)
{
if (item != null) m_ImagePool.ReleaseCell(item.CellId);
if (item != null) m_ImagePool.ReleaseByObject(item);
}
/// <summary>解除指定 Text 的槽位绑定;显隐仅在所属 Counter 完整归还时变更。</summary>
public void ReleaseText(PooledText item)
{
if (item != null) m_TextPool.ReleaseCell(item.CellId);
if (item != null) m_TextPool.ReleaseByObject(item);
}
private void SetImageCounterActive(IReadOnlyList<PooledImage> items)
@@ -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<GameObject> beforeRelease = null)
{
if (instance == null) return false;
@@ -30,10 +30,9 @@ namespace XericUI.XTable.Rendering.Elements
#region
/// <summary>cellId 路由桶数量。</summary>
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<SlotCounter> m_StandbyCounters = new Stack<SlotCounter>();
private readonly Func<T> m_Factory;
@@ -43,16 +42,31 @@ namespace XericUI.XTable.Rendering.Elements
/// <summary>已创建对象总数(只读)</summary>
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<T> factory, int maxCreatedCount = 256,
Action<IReadOnlyList<T>> onCounterBorrowed = null, Action<IReadOnlyList<T>> onCounterReturned = null)
Action<IReadOnlyList<T>> onCounterBorrowed = null, Action<IReadOnlyList<T>> 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
/// <summary>解除全部槽位绑定,并将所有计数器整体归还待命栈。</summary>
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<SlotCounter>();
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)