更换表格对象池系统,新的对象池系统会尽可能减小对象的回收操作,同时引入基于曼哈顿点距的对象池索引逻辑,引入计数器,避免野对象或对象冲突。

This commit is contained in:
2026-07-28 13:52:24 +08:00
parent b6557db618
commit 550f588408
6 changed files with 351 additions and 170 deletions
@@ -229,11 +229,12 @@ namespace XericUI.XTable.Rendering.Component
XTableCellData data = tableData?.GetCell(r, c);
string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace))
? data.StyleNamespace : defaultNs;
int cellId = r * colWidths.Length + c;
// 选择高亮
if (IsInSelectionRange != null && IsInSelectionRange(r, c))
{
var selImg = m_Assembler.GetImage("cell_multiselect");
var selImg = m_Assembler.GetImage(cellId, "cell_multiselect");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
@@ -243,7 +244,7 @@ namespace XericUI.XTable.Rendering.Component
}
else if (r == selRow && c == selCol)
{
var selImg = m_Assembler.GetImage("cell_select");
var selImg = m_Assembler.GetImage(cellId, "cell_select");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
@@ -253,7 +254,7 @@ namespace XericUI.XTable.Rendering.Component
}
// 网格线
var hLine = m_Assembler.GetImage("cell_hline");
var hLine = m_Assembler.GetImage(cellId, "cell_hline");
if (hLine != null)
{
hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH);
@@ -261,7 +262,7 @@ namespace XericUI.XTable.Rendering.Component
hLine.Image.color = borderColor;
}
var vLine = m_Assembler.GetImage("cell_vline");
var vLine = m_Assembler.GetImage(cellId, "cell_vline");
if (vLine != null)
{
vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - borderWidth, cellY);
@@ -276,7 +277,7 @@ namespace XericUI.XTable.Rendering.Component
case CellContentType.Text:
if (!string.IsNullOrEmpty(data.Text))
{
var txt = m_Assembler.GetText("cell_text", cellNS);
var txt = m_Assembler.GetText(cellId, "cell_text", cellNS);
if (txt != null)
{
txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
@@ -296,7 +297,7 @@ namespace XericUI.XTable.Rendering.Component
if (data.Image != null)
{
Sprite sprite = GetOrCreateSprite(data.Image);
var img = m_Assembler.GetImage("cell_image", cellNS);
var img = m_Assembler.GetImage(cellId, "cell_image", cellNS);
if (img != null)
{
img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
@@ -308,7 +309,7 @@ namespace XericUI.XTable.Rendering.Component
case CellContentType.Prefab:
if (data.Prefab != null && PrefabPool != null)
{
GameObject instance = PrefabPool.Acquire(data.Prefab, r, c);
GameObject instance = PrefabPool.Acquire(data.Prefab, cellId);
if (instance != null)
{
RectTransform prt = instance.GetComponent<RectTransform>();
@@ -357,10 +358,11 @@ namespace XericUI.XTable.Rendering.Component
XTableCellData data = tableData?.GetCell(r, c);
string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace))
? data.StyleNamespace : defaultNs;
int cellId = r * colWidths.Length + c;
if (IsInSelectionRange != null && IsInSelectionRange(r, c))
{
var selImg = m_Assembler.GetImage("cell_multiselect");
var selImg = m_Assembler.GetImage(cellId, "cell_multiselect");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
@@ -370,7 +372,7 @@ namespace XericUI.XTable.Rendering.Component
}
else if (r == selRow && c == selCol)
{
var selImg = m_Assembler.GetImage("cell_select");
var selImg = m_Assembler.GetImage(cellId, "cell_select");
if (selImg != null)
{
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
@@ -379,7 +381,7 @@ namespace XericUI.XTable.Rendering.Component
}
}
var hLine = m_Assembler.GetImage("cell_hline");
var hLine = m_Assembler.GetImage(cellId, "cell_hline");
if (hLine != null)
{
hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH);
@@ -387,7 +389,7 @@ namespace XericUI.XTable.Rendering.Component
hLine.Image.color = Config.RuntimeBorderColor;
}
var vLine = m_Assembler.GetImage("cell_vline");
var vLine = m_Assembler.GetImage(cellId, "cell_vline");
if (vLine != null)
{
vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - Config.RuntimeBorderWidth, cellY);
@@ -402,7 +404,7 @@ namespace XericUI.XTable.Rendering.Component
case CellContentType.Text:
if (!string.IsNullOrEmpty(data.Text))
{
var txt = m_Assembler.GetText("cell_text", cellNS);
var txt = m_Assembler.GetText(cellId, "cell_text", cellNS);
if (txt != null)
{
txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
@@ -415,7 +417,7 @@ namespace XericUI.XTable.Rendering.Component
if (data.Image != null)
{
Sprite sprite = GetOrCreateSprite(data.Image);
var img = m_Assembler.GetImage("cell_image", cellNS);
var img = m_Assembler.GetImage(cellId, "cell_image", cellNS);
if (img != null)
{
img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
@@ -427,7 +429,7 @@ namespace XericUI.XTable.Rendering.Component
case CellContentType.Prefab:
if (data.Prefab != null && PrefabPool != null)
{
GameObject instance = PrefabPool.Acquire(data.Prefab, r, c);
GameObject instance = PrefabPool.Acquire(data.Prefab, cellId);
if (instance != null)
{
RectTransform prt = instance.GetComponent<RectTransform>();
@@ -154,14 +154,15 @@ namespace XericUI.XTable.Rendering.Component
cellH += m_RowHeights[row + i];
}
int cellId = row * m_ColWidths.Length + col;
var objs = new CellTrackedObjs();
// ── 选择高亮 ──
ApplySelectionHighlight(row, col, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs);
ApplySelectionHighlight(cellId, row, col, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs);
// ── 网格线(仅完整刷新时)──
if (withGridLines)
ApplyGridLines(cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs);
ApplyGridLines(cellId, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs);
// ── 单元格内容 ──
if (data != null)
@@ -169,13 +170,13 @@ namespace XericUI.XTable.Rendering.Component
switch (data.ContentType)
{
case CellContentType.Text:
RenderCellText(data, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs);
RenderCellText(cellId, data, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs);
break;
case CellContentType.Image:
RenderCellImage(data, cellX, cellY, cellW, cellH, cellNS, ref objs);
RenderCellImage(cellId, data, cellX, cellY, cellW, cellH, cellNS, ref objs);
break;
case CellContentType.Prefab:
RenderCellPrefab(data, row, col, cellX, cellY, cellW, cellH);
RenderCellPrefab(cellId, data, row, col, cellX, cellY, cellW, cellH);
break;
}
}
@@ -184,7 +185,7 @@ namespace XericUI.XTable.Rendering.Component
m_LastRenderedCells.Add((row, col));
}
private void ApplySelectionHighlight(int row, int col,
private void ApplySelectionHighlight(int cellId, int row, int col,
float cellX, float cellY, float cellW, float cellH,
string cellNS, bool editorMode, ref CellTrackedObjs objs)
{
@@ -194,7 +195,7 @@ namespace XericUI.XTable.Rendering.Component
if (!inRange && !isFocused) return;
string key = inRange ? "cell_multiselect" : "cell_select";
var img = m_Assembler.GetImage(key, cellNS);
var img = m_Assembler.GetImage(cellId, key, cellNS);
if (img == null) return;
img.Rect.anchoredPosition = new Vector2(cellX, cellY);
img.Rect.sizeDelta = new Vector2(cellW, cellH);
@@ -222,7 +223,7 @@ namespace XericUI.XTable.Rendering.Component
objs.SelectBg = img;
}
private void ApplyGridLines(float cellX, float cellY, float cellW, float cellH,
private void ApplyGridLines(int cellId, float cellX, float cellY, float cellW, float cellH,
string cellNS, bool editorMode, ref CellTrackedObjs objs)
{
Color borderColor;
@@ -240,7 +241,7 @@ namespace XericUI.XTable.Rendering.Component
}
// 水平网格线(底部)
var hLine = m_Assembler.GetImage("cell_hline", cellNS);
var hLine = m_Assembler.GetImage(cellId, "cell_hline", cellNS);
if (hLine != null)
{
hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH);
@@ -250,7 +251,7 @@ namespace XericUI.XTable.Rendering.Component
}
// 垂直网格线(右侧)
var vLine = m_Assembler.GetImage("cell_vline", cellNS);
var vLine = m_Assembler.GetImage(cellId, "cell_vline", cellNS);
if (vLine != null)
{
vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - borderWidth, cellY);
@@ -260,13 +261,13 @@ namespace XericUI.XTable.Rendering.Component
}
}
private void RenderCellText(XTableCellData data,
private void RenderCellText(int cellId, XTableCellData data,
float cellX, float cellY, float cellW, float cellH,
string cellNS, bool editorMode, ref CellTrackedObjs objs)
{
if (string.IsNullOrEmpty(data.Text)) return;
var txt = m_Assembler.GetText("cell_text", cellNS);
var txt = m_Assembler.GetText(cellId, "cell_text", cellNS);
if (txt == null) return;
txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH);
@@ -283,14 +284,14 @@ namespace XericUI.XTable.Rendering.Component
objs.CellText = txt;
}
private void RenderCellImage(XTableCellData data,
private void RenderCellImage(int cellId, XTableCellData data,
float cellX, float cellY, float cellW, float cellH,
string cellNS, ref CellTrackedObjs objs)
{
if (data.Image == null) return;
Sprite sprite = GetOrCreateSprite(data.Image);
var img = m_Assembler.GetImage("cell_image", cellNS);
var img = m_Assembler.GetImage(cellId, "cell_image", cellNS);
if (img == null) return;
img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY);
img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH);
@@ -299,12 +300,12 @@ namespace XericUI.XTable.Rendering.Component
objs.CellImage = img;
}
private void RenderCellPrefab(XTableCellData data, int row, int col,
private void RenderCellPrefab(int cellId, XTableCellData data, int row, int col,
float cellX, float cellY, float cellW, float cellH)
{
if (data.Prefab == null) return;
GameObject instance = m_PrefabPool.Acquire(data.Prefab, row, col);
GameObject instance = m_PrefabPool.Acquire(data.Prefab, cellId);
if (instance == null) return;
RectTransform prt = instance.GetComponent<RectTransform>();
prt.anchoredPosition = new Vector2(cellX, cellY);
@@ -442,7 +443,7 @@ namespace XericUI.XTable.Rendering.Component
ReleaseTrackedCell(cell.Item1, cell.Item2);
}
// 3. 回收不再可见的预制体
// 3. 回收不再可见的预制体(仅从映射表移除,预制体保留在计数器槽位中复用)
if (m_ActivePrefabMap != null && m_ActivePrefabMap.Count > 0)
{
m_ReusableCellList.Clear();
@@ -452,9 +453,7 @@ namespace XericUI.XTable.Rendering.Component
foreach (var key in m_ReusableCellList)
{
var cellData = TableData.GetCell(key.Item1, key.Item2);
cellData?.OnPrefabRelease?.Invoke(m_ActivePrefabMap[key]);
m_PrefabPool.Release(m_ActivePrefabMap[key]);
// 不调用 OnPrefabRelease 和 Release——预制体保留在槽位中
m_ActivePrefabMap.Remove(key);
}
}
@@ -1,5 +1,3 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
@@ -8,7 +6,8 @@ using UnityEngine.UI;
namespace XericUI.XTable.Rendering.Elements
{
/// <summary>
/// 表格单元动态拼装器——通过对象池管理 Image + TMP_Text 的组合创建与回收。
/// 表格单元动态拼装器——通过 SlotCounterPool 管理 Image + TMP_Text 的组合创建与回收。
/// 每个单元格通过 cellId 映射到固定索引的计数器槽位,空闲槽位经 FrozenSlots 全局流转复用。
/// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。
/// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。
/// </summary>
@@ -22,6 +21,8 @@ namespace XericUI.XTable.Rendering.Elements
public RectTransform Rect;
public Image Image;
public StyleBoundElement Style;
/// <summary>绑定的单元格 ID,用于 Release 定位槽位</summary>
public int CellId;
}
public class PooledText
@@ -30,6 +31,8 @@ namespace XericUI.XTable.Rendering.Elements
public RectTransform Rect;
public TMP_Text Text;
public StyleBoundElement Style;
/// <summary>绑定的单元格 ID,用于 Release 定位槽位</summary>
public int CellId;
}
#endregion
@@ -44,20 +47,14 @@ namespace XericUI.XTable.Rendering.Elements
/// <summary>所有已绑定的 StyleBoundElement 回调集合(用于样式变更时通知外部)</summary>
public event System.Action OnStyleChangeDetected;
private Queue<PooledImage> m_ImagePool = new Queue<PooledImage>();
private Queue<PooledText> m_TextPool = new Queue<PooledText>();
private List<PooledImage> m_ActiveImages = new List<PooledImage>();
private List<PooledText> m_ActiveTexts = new List<PooledText>();
/// <summary>基于计数器的 Image 槽位对象池</summary>
private SlotCounterPool<PooledImage> m_ImagePool;
/// <summary>基于计数器的 Text 槽位对象池</summary>
private SlotCounterPool<PooledText> m_TextPool;
/// <summary>HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示)</summary>
private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy;
/// <summary>渲染项目总数上限,超出后 GetImage/GetText 返回 null 并发出警告</summary>
private const int MAX_POOL_ITEMS = 256;
private int m_ImageCreatedCount;
private int m_TextCreatedCount;
#endregion
#region 构造函数与析构
@@ -66,33 +63,15 @@ namespace XericUI.XTable.Rendering.Elements
{
m_Parent = parent;
m_DefaultStyleNamespace = defaultStyleNamespace;
m_ImagePool = new SlotCounterPool<PooledImage>(() => CreateImage("cell_img"));
m_TextPool = new SlotCounterPool<PooledText>(() => CreateText("cell_text"));
}
/// <summary>彻底销毁所有池对象(包括活跃和休眠的)</summary>
/// <summary>彻底销毁所有池对象(包括计数器内活跃槽位和冻结槽位)</summary>
public void DestroyAll()
{
// 杀死活跃对象
for (int i = m_ActiveImages.Count - 1; i >= 0; i--)
{
KillObject(m_ActiveImages[i].Root);
}
m_ActiveImages.Clear();
for (int i = m_ActiveTexts.Count - 1; i >= 0; i--)
{
KillObject(m_ActiveTexts[i].Root);
}
m_ActiveTexts.Clear();
// 杀死池中对象
while (m_ImagePool.Count > 0)
{
KillObject(m_ImagePool.Dequeue().Root);
}
while (m_TextPool.Count > 0)
{
KillObject(m_TextPool.Dequeue().Root);
}
m_ImagePool.DestroyAll(item => KillObject(item.Root));
m_TextPool.DestroyAll(item => KillObject(item.Root));
}
private static void KillObject(GameObject go)
@@ -116,49 +95,29 @@ namespace XericUI.XTable.Rendering.Elements
#region 池操作
public PooledImage GetImage(string name = "cell_img", string styleNamespace = null)
/// <summary>为指定 cellId 获取一个池化 Image</summary>
public PooledImage GetImage(int cellId, string name = "cell_img", string styleNamespace = null)
{
PooledImage item;
if (m_ImagePool.Count > 0)
{
item = m_ImagePool.Dequeue();
ResetImageDefaults(item);
}
else if (m_ImageCreatedCount < MAX_POOL_ITEMS)
{
item = CreateImage(name);
m_ImageCreatedCount++;
}
else
{
Debug.LogWarning($"[XericUIActionTable] Image 池已达上限 {MAX_POOL_ITEMS},无法分配新对象");
return null;
}
var item = m_ImagePool.Acquire(cellId);
if (item == null) return null;
item.CellId = cellId;
item.Root.name = name;
ResetImageDefaults(item);
BindStyle(item.Style, styleNamespace);
m_ActiveImages.Add(item);
return item;
}
public PooledText GetText(string name = "cell_text", string styleNamespace = null)
/// <summary>为指定 cellId 获取一个池化 TMP_Text</summary>
public PooledText GetText(int cellId, string name = "cell_text", string styleNamespace = null)
{
PooledText item;
if (m_TextPool.Count > 0)
{
item = m_TextPool.Dequeue();
ResetTextDefaults(item);
}
else if (m_TextCreatedCount < MAX_POOL_ITEMS)
{
item = CreateText(name);
m_TextCreatedCount++;
}
else
{
Debug.LogWarning($"[XericUIActionTable] Text 池已达上限 {MAX_POOL_ITEMS},无法分配新对象");
return null;
}
var item = m_TextPool.Acquire(cellId);
if (item == null) return null;
item.CellId = cellId;
item.Root.name = name;
ResetTextDefaults(item);
BindStyle(item.Style, styleNamespace);
m_ActiveTexts.Add(item);
return item;
}
@@ -183,41 +142,28 @@ namespace XericUI.XTable.Rendering.Elements
OnStyleChangeDetected?.Invoke();
}
/// <summary>归还所有槽位至 FrozenSlots(全量刷新时调用)</summary>
public void ReturnAll()
{
foreach (var img in m_ActiveImages) ReturnImage(img);
m_ActiveImages.Clear();
foreach (var txt in m_ActiveTexts) ReturnText(txt);
m_ActiveTexts.Clear();
// Unbind 所有活跃槽位的样式回调
m_ImagePool.ReturnAll();
m_TextPool.ReturnAll();
}
/// <summary>归还单个 Image 池对象(从活跃列表移除并推入休眠栈)</summary>
/// <summary>归还单个 Image 池对象(通过 CellId 定位槽位并移入 FrozenSlots)</summary>
public void ReleaseImage(PooledImage item)
{
if (item == null) return;
m_ActiveImages.Remove(item);
ReturnImage(item);
item.Style?.Unbind();
m_ImagePool.Release(item.CellId);
}
/// <summary>归还单个 Text 池对象(从活跃列表移除并推入休眠栈)</summary>
/// <summary>归还单个 Text 池对象(通过 CellId 定位槽位并移入 FrozenSlots)</summary>
public void ReleaseText(PooledText item)
{
if (item == null) return;
m_ActiveTexts.Remove(item);
ReturnText(item);
}
private void ReturnImage(PooledImage item)
{
item.Style?.Unbind();
m_ImagePool.Enqueue(item);
}
private void ReturnText(PooledText item)
{
item.Style?.Unbind();
m_TextPool.Enqueue(item);
m_TextPool.Release(item.CellId);
}
#endregion
@@ -5,8 +5,8 @@ using UnityEngine;
namespace XericUI.XTable.Rendering.Elements
{
/// <summary>
/// 预制体对象池——按原型分组管理,每个预制体对应一个独立的对象池。
/// 使用 Queue + HashSet 模式,FIFO 保证对象按回收顺序均匀复用。
/// 预制体对象池——按原型分组管理,每个原型使用计数器槽位绑定 cellId 到预制体实例。
/// 预制体不参与 FrozenSlots 流转:始终绑定在 cellId 槽位上,仅在表格销毁时释放。
/// </summary>
public class PrefabPrototypePool
{
@@ -15,10 +15,8 @@ namespace XericUI.XTable.Rendering.Elements
private class PrefabPool
{
public GameObject Prototype;
/// <summary>休眠实例队列(FIFO,先归还的优先复用)</summary>
public Queue<GameObject> Available = new Queue<GameObject>();
/// <summary>活跃实例集合(O(1) 查找 + 防止重复归还)</summary>
public HashSet<GameObject> Active = new HashSet<GameObject>();
/// <summary>cellId → 预制体实例 的计数器槽位绑定</summary>
public Dictionary<int, GameObject> Slots = new Dictionary<int, GameObject>();
}
#endregion
@@ -28,7 +26,7 @@ namespace XericUI.XTable.Rendering.Elements
private Dictionary<GameObject, PrefabPool> m_Pools = new Dictionary<GameObject, PrefabPool>();
private Transform m_Parent;
/// <summary>渲染项目总数上限</summary>
/// <summary>渲染项目总数上限(全局,跨所有原型)</summary>
private const int MAX_PREFAB_ITEMS = 256;
private int m_TotalCreated;
@@ -46,21 +44,18 @@ namespace XericUI.XTable.Rendering.Elements
m_Parent = parent;
}
/// <summary>彻底销毁所有池对象(活跃 + 休眠)</summary>
/// <summary>彻底销毁所有池对象(所有原型的活跃槽位)</summary>
public void DestroyAll()
{
foreach (var kv in m_Pools)
{
var pool = kv.Value;
// 销毁活跃实例
foreach (var go in pool.Active)
KillObject(go);
pool.Active.Clear();
// 销毁休眠实例
while (pool.Available.Count > 0)
KillObject(pool.Available.Dequeue());
foreach (var instance in pool.Slots.Values)
KillObject(instance);
pool.Slots.Clear();
}
m_Pools.Clear();
m_TotalCreated = 0;
}
private static void KillObject(GameObject go)
@@ -76,31 +71,31 @@ namespace XericUI.XTable.Rendering.Elements
#region 池操作
/// <summary>从池中获取实例(FIFO 复用;池空且未达上限则 Instantiate;已达上限返回 null)</summary>
public GameObject Acquire(GameObject prefab, int row, int col)
/// <summary>
/// 为指定 cellId 获取预制体实例。
/// 若该 cellId 已有绑定的实例,直接返回;否则创建新实例(需未达上限)。
/// </summary>
public GameObject Acquire(GameObject prefab, int cellId)
{
if (prefab == null) return null;
var pool = GetOrCreatePool(prefab);
GameObject instance;
if (pool.Available.Count > 0)
{
instance = pool.Available.Dequeue();
}
else if (m_TotalCreated < MAX_PREFAB_ITEMS)
{
instance = Object.Instantiate(prefab, m_Parent);
instance.hideFlags = POOL_FLAGS;
m_TotalCreated++;
}
else
// 已绑定 → 直接复用
if (pool.Slots.TryGetValue(cellId, out var existing))
return existing;
// 创建新实例
if (m_TotalCreated >= MAX_PREFAB_ITEMS)
{
Debug.LogWarning($"[XericUIActionTable] Prefab 池已达上限 {MAX_PREFAB_ITEMS},无法分配新实例");
return null;
}
instance.name = $"_prefab_{row}_{col}";
var instance = Object.Instantiate(prefab, m_Parent);
instance.hideFlags = POOL_FLAGS;
instance.name = $"_prefab_{cellId}";
m_TotalCreated++;
// 强制 RectTransform 左上角坐标系
RectTransform rt = instance.GetComponent<RectTransform>();
@@ -109,21 +104,36 @@ namespace XericUI.XTable.Rendering.Elements
rt.anchorMax = AnchorTopLeft;
rt.pivot = PivotTopLeft;
pool.Active.Add(instance);
pool.Slots[cellId] = instance;
return instance;
}
/// <summary>将指定实例归还对象池(不入池根,原地保留避免 OnEnable/OnDisable)</summary>
/// <summary>
/// 归还指定实例至槽位(仅从槽位移除 + 销毁对象,不在滚动时调用)。
/// 预制体的回收仅发生在表格销毁时。
/// </summary>
public void Release(GameObject instance)
{
if (instance == null) return;
// 查找所属池
foreach (var kv in m_Pools)
{
if (kv.Value.Active.Remove(instance))
var pool = kv.Value;
// 查找并移除
int? foundKey = null;
foreach (var slot in pool.Slots)
{
kv.Value.Available.Enqueue(instance);
if (slot.Value == instance)
{
foundKey = slot.Key;
break;
}
}
if (foundKey.HasValue)
{
pool.Slots.Remove(foundKey.Value);
m_TotalCreated--;
KillObject(instance);
return;
}
}
@@ -132,18 +142,17 @@ namespace XericUI.XTable.Rendering.Elements
KillObject(instance);
}
/// <summary>将所有活跃实例回收至池(原地保留,仅从活跃集移除并入队)</summary>
/// <summary>将所有活跃槽位清空并销毁对象</summary>
public void ReturnAll()
{
foreach (var kv in m_Pools)
{
var pool = kv.Value;
foreach (var go in pool.Active)
{
pool.Available.Enqueue(go);
}
pool.Active.Clear();
foreach (var instance in pool.Slots.Values)
KillObject(instance);
pool.Slots.Clear();
}
m_TotalCreated = 0;
}
/// <summary>获取当前所有活跃实例(用于外部遍历回调)</summary>
@@ -151,7 +160,7 @@ namespace XericUI.XTable.Rendering.Elements
{
if (prefab == null || !m_Pools.TryGetValue(prefab, out var pool))
return System.Array.Empty<GameObject>();
return pool.Active;
return pool.Slots.Values;
}
#endregion
@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XTable.Rendering.Elements
{
/// <summary>
/// 基于计数器的槽位对象池(泛型)。
/// 每个 cellId 映射到固定索引的 SlotCounter,槽位在计数器间通过 FrozenSlots 流转复用。
/// 避免 Queue 模式下对象与位置解耦导致的堆叠问题,同时不触发 SetActive/SetParent 等生命周期事件。
/// </summary>
/// <typeparam name="T">池对象类型</typeparam>
public class SlotCounterPool<T> where T : class
{
#region 内部类型
private class Slot
{
/// <summary>当前绑定的单元格 ID(-1 = 空闲,可被偷取)</summary>
public int CellId = -1;
/// <summary>池对象引用</summary>
public T PooledObject;
}
private class SlotCounter
{
public List<Slot> Slots = new List<Slot>(capacity: 4);
}
#endregion
#region 字段
/// <summary>计数器数组大小(= 池上限,也是 cellId 取模的模数)</summary>
public const int POOL_SIZE = 256;
private SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE];
/// <summary>冻结槽位列表——全局可偷取的休眠槽位(末尾偷取,避免 Canvas 重建)</summary>
private List<Slot> m_FrozenSlots = new List<Slot>();
/// <summary>已创建对象总数(不含预制体 Instantiate)</summary>
private int m_CreatedCount;
/// <summary>对象工厂(构造函数传入)</summary>
private Func<T> m_Factory;
/// <summary>已创建对象总数(只读)</summary>
public int CreatedCount => m_CreatedCount;
#endregion
#region 构造
public SlotCounterPool(Func<T> factory)
{
m_Factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
#endregion
#region 池操作
/// <summary>
/// 为指定 cellId 获取一个池对象。
/// 优先复用本计数器内的空闲槽位,其次从 FrozenSlots 偷取,最后创建新对象。
/// </summary>
public T Acquire(int cellId)
{
int index = cellId % POOL_SIZE;
if (index < 0) index += POOL_SIZE; // 处理负 cellId
var counter = m_Counters[index];
if (counter == null)
{
counter = new SlotCounter();
m_Counters[index] = counter;
}
// 1. 在计数器内查找空闲槽位
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;
}
}
// 2. 从 FrozenSlots 末尾偷取
if (m_FrozenSlots.Count > 0)
{
int lastIdx = m_FrozenSlots.Count - 1;
var stolen = m_FrozenSlots[lastIdx];
m_FrozenSlots.RemoveAt(lastIdx);
stolen.CellId = cellId;
counter.Slots.Add(stolen);
return stolen.PooledObject;
}
// 3. 创建新对象
if (m_CreatedCount < POOL_SIZE)
{
var obj = m_Factory();
var slot = new Slot { CellId = cellId, PooledObject = obj };
counter.Slots.Add(slot);
m_CreatedCount++;
return obj;
}
// 4. 已达上限
Debug.LogWarning($"[SlotCounterPool<{typeof(T).Name}>] 已达上限 {POOL_SIZE},无法分配新对象");
return null;
}
/// <summary>
/// 释放指定 cellId 占用的槽位,将其移入 FrozenSlots 供其他计数器复用。
/// </summary>
public void Release(int cellId)
{
int index = cellId % POOL_SIZE;
if (index < 0) index += POOL_SIZE;
var counter = m_Counters[index];
if (counter == null) return;
for (int i = 0; i < counter.Slots.Count; i++)
{
if (counter.Slots[i].CellId == cellId)
{
var slot = counter.Slots[i];
counter.Slots.RemoveAt(i);
slot.CellId = -1;
m_FrozenSlots.Add(slot);
return;
}
}
}
/// <summary>
/// 尝试通过池对象引用查找并释放对应槽位。
/// 用于 Release(PooledObject) 场景,需要遍历查找。
/// </summary>
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))
{
var slot = counter.Slots[i];
counter.Slots.RemoveAt(i);
slot.CellId = -1;
m_FrozenSlots.Add(slot);
return true;
}
}
}
return false;
}
/// <summary>将所有活跃槽位移入 FrozenSlots</summary>
public void ReturnAll()
{
for (int ci = 0; ci < POOL_SIZE; ci++)
{
var counter = m_Counters[ci];
if (counter == null) continue;
for (int i = counter.Slots.Count - 1; i >= 0; i--)
{
var slot = counter.Slots[i];
counter.Slots.RemoveAt(i);
slot.CellId = -1;
m_FrozenSlots.Add(slot);
}
}
}
/// <summary>
/// 彻底销毁所有池对象(包括计数器内活跃槽位和 FrozenSlots 中的冻结槽位)。
/// </summary>
public void DestroyAll(Action<T> destroyFunc)
{
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();
}
foreach (var slot in m_FrozenSlots)
destroyFunc(slot.PooledObject);
m_FrozenSlots.Clear();
m_CreatedCount = 0;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91facbccfc5ebff44b47c4b4fe51135b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: