优化ObjectPool

This commit is contained in:
陈思海 2026-04-24 14:33:26 +08:00
parent ecd9dfc970
commit 4d73c62440
14 changed files with 674 additions and 87 deletions

View File

@ -28,11 +28,6 @@ namespace AlicizaX
/// <param name="name">名称。</param>
public TypeNamePair(Type type, string name)
{
if (type == null)
{
throw new GameFrameworkException("Type is invalid.");
}
m_Type = type;
m_Name = name ?? string.Empty;
}
@ -61,11 +56,21 @@ namespace AlicizaX
{
if (m_Type == null)
{
throw new GameFrameworkException("Type is invalid.");
return string.Empty;
}
string typeName = m_Type.FullName;
return (string.IsNullOrEmpty(m_Name) ? typeName : Utility.Text.Format("{0}.{1}", typeName, m_Name)) ?? string.Empty;
if (string.IsNullOrEmpty(m_Name))
return typeName ?? string.Empty;
// 使用 ZString 避免字符串分配
using (var sb = Cysharp.Text.ZString.CreateStringBuilder())
{
sb.Append(typeName);
sb.Append('.');
sb.Append(m_Name);
return sb.ToString();
}
}
/// <summary>

View File

@ -9,6 +9,7 @@ namespace AlicizaX.ObjectPool
public readonly int? Capacity;
public readonly float? ExpireTime;
public readonly int Priority;
public readonly ReleaseStrategy ReleaseStrategy;
public ObjectPoolCreateOptions(
string name = "",
@ -16,7 +17,8 @@ namespace AlicizaX.ObjectPool
float? autoReleaseInterval = null,
int? capacity = null,
float? expireTime = null,
int priority = 0)
int priority = 0,
ReleaseStrategy releaseStrategy = ReleaseStrategy.LRU)
{
Name = name ?? string.Empty;
AllowMultiSpawn = allowMultiSpawn;
@ -24,10 +26,11 @@ namespace AlicizaX.ObjectPool
Capacity = capacity;
ExpireTime = expireTime;
Priority = priority;
ReleaseStrategy = releaseStrategy;
}
public ObjectPoolCreateOptions WithName(string name)
=> new ObjectPoolCreateOptions(name, AllowMultiSpawn, AutoReleaseInterval, Capacity, ExpireTime, Priority);
=> new ObjectPoolCreateOptions(name, AllowMultiSpawn, AutoReleaseInterval, Capacity, ExpireTime, Priority, ReleaseStrategy);
public static ObjectPoolCreateOptions Single(string name = "")
=> new ObjectPoolCreateOptions(name: name);

View File

@ -0,0 +1,18 @@
namespace AlicizaX.ObjectPool
{
/// <summary>
/// 可池化对象接口,支持自定义回收和重用逻辑
/// </summary>
public interface IPoolableObject
{
/// <summary>
/// 对象被回收到池中时调用(重置状态)
/// </summary>
void OnRecycle();
/// <summary>
/// 对象从池中取出时调用(初始化状态)
/// </summary>
void OnReuse();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c4c7354bf5820e408151d8ecbd1bab1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -54,4 +54,71 @@ namespace AlicizaX.ObjectPool
m_LastUseTime = 0f;
}
}
/// <summary>
/// 泛型对象池基类,消除装箱开销
/// </summary>
public abstract class ObjectBase<T> : IMemory where T : class
{
private string m_Name;
private T m_Target;
private bool m_Locked;
private float m_LastUseTime;
public string Name => m_Name;
public T Target => m_Target;
public bool Locked
{
get => m_Locked;
set => m_Locked = value;
}
public float LastUseTime
{
get => m_LastUseTime;
internal set => m_LastUseTime = value;
}
public virtual bool CustomCanReleaseFlag => true;
protected void Initialize(T target)
{
Initialize(string.Empty, target, false);
}
protected void Initialize(string name, T target)
{
Initialize(name, target, false);
}
protected void Initialize(string name, T target, bool locked)
{
m_Name = name ?? string.Empty;
m_Target = target;
m_Locked = locked;
m_LastUseTime = 0f;
if (target is IPoolableObject poolable)
poolable.OnReuse();
}
protected internal virtual void OnSpawn() { }
protected internal virtual void OnUnspawn()
{
if (m_Target is IPoolableObject poolable)
poolable.OnRecycle();
}
protected internal abstract void Release(bool isShutdown);
public virtual void Clear()
{
m_Name = null;
m_Target = null;
m_Locked = false;
m_LastUseTime = 0f;
}
}
}

View File

@ -21,7 +21,18 @@ namespace AlicizaX.ObjectPool
get
{
if (m_FullName == null)
m_FullName = new TypeNamePair(ObjectType, m_Name).ToString();
{
using (var sb = Cysharp.Text.ZString.CreateStringBuilder())
{
sb.Append(ObjectType.FullName);
if (!string.IsNullOrEmpty(m_Name))
{
sb.Append('.');
sb.Append(m_Name);
}
m_FullName = sb.ToString();
}
}
return m_FullName;
}
}

View File

@ -36,19 +36,21 @@ namespace AlicizaX.ObjectPool
private ObjectSlot[] m_Slots;
private int m_SlotCount;
private int m_SlotCapacity;
private int[] m_FreeStack;
private int m_FreeTop;
private readonly Dictionary<object, int> m_TargetMap;
private readonly Dictionary<string, int> m_AllNameHeadMap;
private readonly Dictionary<string, int> m_AvailableNameHeadMap;
private readonly Dictionary<string, int> m_AvailableNameTailMap;
private IntOpenHashMap m_TargetMap;
private StringOpenHashMap m_AllNameHeadMap;
private StringOpenHashMap m_AvailableNameHeadMap;
private StringOpenHashMap m_AvailableNameTailMap;
private readonly bool m_AllowMultiSpawn;
private float m_AutoReleaseInterval;
private int m_Capacity;
private float m_ExpireTime;
private int m_Priority;
private ReleaseStrategy m_ReleaseStrategy;
private float m_AutoReleaseTime;
private int m_PendingReleaseCount;
@ -57,26 +59,30 @@ namespace AlicizaX.ObjectPool
private int m_UnusedTail;
private int m_LastBudgetScanStart;
private bool m_IsShuttingDown;
private int m_ShrinkCounter;
private const int ShrinkCheckInterval = 60;
private const int DefaultReleasePerFrame = 8;
private const int InitSlotCapacity = 16;
public ObjectPool(string name, bool allowMultiSpawn,
float autoReleaseInterval, int capacity, float expireTime, int priority)
float autoReleaseInterval, int capacity, float expireTime, int priority, ReleaseStrategy releaseStrategy)
: base(name)
{
int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity);
m_Slots = new ObjectSlot[initCap];
m_FreeStack = new int[initCap];
m_TargetMap = new Dictionary<object, int>(initCap, AlicizaX.ReferenceComparer<object>.Instance);
m_AllNameHeadMap = new Dictionary<string, int>(initCap, StringComparer.Ordinal);
m_AvailableNameHeadMap = new Dictionary<string, int>(initCap, StringComparer.Ordinal);
m_AvailableNameTailMap = new Dictionary<string, int>(initCap, StringComparer.Ordinal);
m_SlotCapacity = initCap;
m_Slots = SlotArrayPool<ObjectSlot>.Rent(initCap);
m_FreeStack = SlotArrayPool<int>.Rent(initCap);
m_TargetMap = new IntOpenHashMap(initCap);
m_AllNameHeadMap = new StringOpenHashMap(initCap);
m_AvailableNameHeadMap = new StringOpenHashMap(initCap);
m_AvailableNameTailMap = new StringOpenHashMap(initCap);
m_AllowMultiSpawn = allowMultiSpawn;
m_AutoReleaseInterval = autoReleaseInterval;
m_Capacity = capacity;
m_ExpireTime = expireTime;
m_Priority = priority;
m_ReleaseStrategy = releaseStrategy;
m_AutoReleaseTime = 0f;
m_PendingReleaseCount = 0;
m_ReleasePerFrameBudget = DefaultReleasePerFrame;
@ -84,6 +90,7 @@ namespace AlicizaX.ObjectPool
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
m_IsShuttingDown = false;
m_ShrinkCounter = 0;
}
public override Type ObjectType => typeof(T);
@ -95,7 +102,13 @@ namespace AlicizaX.ObjectPool
get => m_AutoReleaseInterval;
set
{
if (value < 0f) throw new GameFrameworkException("AutoReleaseInterval is invalid.");
if (value < 0f)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("AutoReleaseInterval is invalid.");
#endif
return;
}
m_AutoReleaseInterval = value;
}
}
@ -105,7 +118,13 @@ namespace AlicizaX.ObjectPool
get => m_Capacity;
set
{
if (value < 0) throw new GameFrameworkException("Capacity is invalid.");
if (value < 0)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Capacity is invalid.");
#endif
return;
}
m_Capacity = value;
if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
}
@ -116,7 +135,13 @@ namespace AlicizaX.ObjectPool
get => m_ExpireTime;
set
{
if (value < 0f) throw new GameFrameworkException("ExpireTime is invalid.");
if (value < 0f)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("ExpireTime is invalid.");
#endif
return;
}
m_ExpireTime = value;
}
}
@ -135,11 +160,17 @@ namespace AlicizaX.ObjectPool
public void Register(T obj, bool spawned)
{
if (obj == null) throw new GameFrameworkException("Object is invalid.");
if (obj.Target == null) throw new GameFrameworkException("Object target is invalid.");
if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && m_Slots[existingIdx].IsAlive())
throw new GameFrameworkException(
$"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'.");
if (obj == null) return;
if (obj.Target == null) return;
int targetHash = obj.Target.GetHashCode();
if (m_TargetMap.TryGetValue(targetHash, out int existingIdx) && m_Slots[existingIdx].IsAlive())
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'.");
#endif
return;
}
int idx = AllocSlot();
ref var slot = ref m_Slots[idx];
@ -154,7 +185,7 @@ namespace AlicizaX.ObjectPool
slot.NextUnused = -1;
slot.SetAlive(true);
m_TargetMap[obj.Target] = idx;
m_TargetMap.AddOrUpdate(targetHash, idx);
string objectName = obj.Name ?? string.Empty;
if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead))
@ -162,7 +193,7 @@ namespace AlicizaX.ObjectPool
m_Slots[existingHead].PrevByName = idx;
slot.NextByName = existingHead;
}
m_AllNameHeadMap[objectName] = idx;
m_AllNameHeadMap.AddOrUpdate(objectName, idx);
obj.LastUseTime = slot.LastUseTime;
if (spawned)
@ -179,14 +210,19 @@ namespace AlicizaX.ObjectPool
public T Spawn(string name)
{
if (name == null) throw new GameFrameworkException("Name is invalid.");
if (name == null) return null;
if (m_AllowMultiSpawn)
return SpawnAny(name);
if (!m_AvailableNameHeadMap.TryGetValue(name, out int head)) return null;
ref var slot = ref m_Slots[head];
if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
throw new GameFrameworkException($"Object pool '{FullName}' available-name head is inconsistent.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is inconsistent.");
#endif
return null;
}
float now = Time.realtimeSinceStartup;
SpawnSlot(head, now);
@ -199,7 +235,7 @@ namespace AlicizaX.ObjectPool
public bool CanSpawn(string name)
{
if (name == null) throw new GameFrameworkException("Name is invalid.");
if (name == null) return false;
if (m_AllowMultiSpawn)
return m_AllNameHeadMap.ContainsKey(name);
@ -208,18 +244,21 @@ namespace AlicizaX.ObjectPool
public void Unspawn(T obj)
{
if (obj == null) throw new GameFrameworkException("Object is invalid.");
if (obj == null) return;
Unspawn(obj.Target);
}
public void Unspawn(object target)
{
if (target == null) throw new GameFrameworkException("Target is invalid.");
if (!m_TargetMap.TryGetValue(target, out int idx))
if (target == null) return;
int targetHash = target.GetHashCode();
if (!m_TargetMap.TryGetValue(targetHash, out int idx))
{
if (m_IsShuttingDown) return;
throw new GameFrameworkException(
$"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'");
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'");
#endif
return;
}
UnspawnSlot(idx);
@ -270,7 +309,11 @@ namespace AlicizaX.ObjectPool
}
bool checkExpire = m_ExpireTime < float.MaxValue;
if (m_PendingReleaseCount <= 0 && !checkExpire) return;
if (m_PendingReleaseCount <= 0 && !checkExpire)
{
TryProgressiveShrink();
return;
}
float now = Time.realtimeSinceStartup;
float expireThreshold = checkExpire ? now - m_ExpireTime : float.MinValue;
@ -284,6 +327,41 @@ namespace AlicizaX.ObjectPool
{
ReleaseExpired(m_ReleasePerFrameBudget, expireThreshold);
}
TryProgressiveShrink();
}
private void TryProgressiveShrink()
{
m_ShrinkCounter++;
if (m_ShrinkCounter < ShrinkCheckInterval)
return;
m_ShrinkCounter = 0;
if (m_TargetMap.Count == 0 || m_SlotCapacity <= InitSlotCapacity)
return;
float usageRatio = (float)m_TargetMap.Count / m_SlotCapacity;
if (usageRatio < 0.25f)
{
int targetCapacity = Math.Max(m_SlotCapacity / 2, InitSlotCapacity);
if (targetCapacity < m_SlotCapacity)
{
var newSlots = SlotArrayPool<ObjectSlot>.Rent(targetCapacity);
var newFreeStack = SlotArrayPool<int>.Rent(targetCapacity);
Array.Copy(m_Slots, 0, newSlots, 0, Math.Min(m_SlotCount, targetCapacity));
Array.Copy(m_FreeStack, 0, newFreeStack, 0, Math.Min(m_FreeTop, targetCapacity));
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = newSlots;
m_FreeStack = newFreeStack;
m_SlotCapacity = targetCapacity;
}
}
}
internal override void Shutdown()
@ -303,7 +381,14 @@ namespace AlicizaX.ObjectPool
m_AllNameHeadMap.Clear();
m_AvailableNameHeadMap.Clear();
m_AvailableNameTailMap.Clear();
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = null;
m_FreeStack = null;
m_SlotCount = 0;
m_SlotCapacity = 0;
m_FreeTop = 0;
m_PendingReleaseCount = 0;
m_UnusedHead = -1;
@ -315,7 +400,13 @@ namespace AlicizaX.ObjectPool
public override int GetAllObjectInfos(ObjectInfo[] results)
{
if (results == null) throw new GameFrameworkException("Results is invalid.");
if (results == null)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Results is invalid.");
#endif
return 0;
}
int write = 0;
int capacity = results.Length;
@ -365,7 +456,12 @@ namespace AlicizaX.ObjectPool
slot.Obj.OnUnspawn();
slot.SpawnCount--;
if (slot.SpawnCount < 0)
throw new GameFrameworkException($"Object '{slot.Obj.Name}' spawn count < 0.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object '{slot.Obj.Name}' spawn count < 0.");
#endif
slot.SpawnCount = 0;
}
if (slot.SpawnCount == 0)
MarkSlotAvailable(idx);
if (Count > m_Capacity && slot.SpawnCount == 0)
@ -393,9 +489,19 @@ namespace AlicizaX.ObjectPool
private void GrowSlots()
{
int newCap = Math.Max(m_Slots.Length * 2, InitSlotCapacity);
Array.Resize(ref m_Slots, newCap);
Array.Resize(ref m_FreeStack, newCap);
int newCap = Math.Max(m_SlotCapacity * 2, InitSlotCapacity);
var newSlots = SlotArrayPool<ObjectSlot>.Rent(newCap);
var newFreeStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount);
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = newSlots;
m_FreeStack = newFreeStack;
m_SlotCapacity = newCap;
}
private void ReleaseSlot(int idx)
@ -408,7 +514,8 @@ namespace AlicizaX.ObjectPool
MarkSlotUnavailable(idx);
RemoveFromAllNameChain(idx);
m_TargetMap.Remove(obj.Target);
int targetHash = obj.Target.GetHashCode();
m_TargetMap.Remove(targetHash);
obj.Release(false);
MemoryPool.Release(obj);
@ -423,8 +530,15 @@ namespace AlicizaX.ObjectPool
slot.PrevUnused = -1;
slot.NextUnused = -1;
if (m_FreeTop >= m_FreeStack.Length)
Array.Resize(ref m_FreeStack, m_FreeStack.Length * 2);
if (m_FreeTop >= m_SlotCapacity)
{
int newCap = m_SlotCapacity * 2;
var newFreeStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_FreeStack = newFreeStack;
m_SlotCapacity = newCap;
}
m_FreeStack[m_FreeTop++] = idx;
ShrinkStorageIfEmpty();
@ -446,10 +560,15 @@ namespace AlicizaX.ObjectPool
else
{
if (head != idx)
throw new GameFrameworkException($"Object pool '{FullName}' all-name chain is inconsistent.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain is inconsistent.");
#endif
return;
}
if (next >= 0)
m_AllNameHeadMap[objectName] = next;
m_AllNameHeadMap.AddOrUpdate(objectName, next);
else
m_AllNameHeadMap.Remove(objectName);
}
@ -524,11 +643,15 @@ namespace AlicizaX.ObjectPool
private void ShrinkStorageIfEmpty()
{
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity)
if (m_TargetMap.Count > 0 || m_SlotCapacity <= InitSlotCapacity)
return;
m_Slots = new ObjectSlot[InitSlotCapacity];
m_FreeStack = new int[InitSlotCapacity];
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_SlotCapacity = InitSlotCapacity;
m_Slots = SlotArrayPool<ObjectSlot>.Rent(InitSlotCapacity);
m_FreeStack = SlotArrayPool<int>.Rent(InitSlotCapacity);
m_AllNameHeadMap.Clear();
m_AvailableNameHeadMap.Clear();
m_AvailableNameTailMap.Clear();
@ -542,6 +665,9 @@ namespace AlicizaX.ObjectPool
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
private void ValidateState()
{
#if !ENABLE_OBJECTPOOL_VALIDATION
return;
#else
int aliveCount = 0;
int unusedCount = 0;
for (int i = 0; i < m_SlotCount; i++)
@ -553,18 +679,29 @@ namespace AlicizaX.ObjectPool
aliveCount++;
object target = slot.Obj.Target;
if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != i)
throw new GameFrameworkException($"Object pool '{FullName}' target index map is inconsistent.");
int targetHash = target.GetHashCode();
if (!m_TargetMap.TryGetValue(targetHash, out int mappedIdx) || mappedIdx != i)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent.");
continue;
}
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
throw new GameFrameworkException($"Object pool '{FullName}' all-name head is missing.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing.");
continue;
}
if (slot.PrevByName < 0 && head != i)
throw new GameFrameworkException($"Object pool '{FullName}' all-name chain head is inconsistent.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent.");
}
if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i)
throw new GameFrameworkException($"Object pool '{FullName}' all-name chain link is inconsistent.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent.");
}
bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
bool inAvailableList = false;
@ -573,30 +710,46 @@ namespace AlicizaX.ObjectPool
{
unusedCount++;
if (!inUnusedList)
throw new GameFrameworkException($"Object pool '{FullName}' unused list is inconsistent.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent.");
}
if (!m_AvailableNameHeadMap.TryGetValue(objectName, out int availableHead))
throw new GameFrameworkException($"Object pool '{FullName}' available-name head is missing.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is missing.");
}
else
{
inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0;
if (!inAvailableList)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent.");
}
inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0;
if (!inAvailableList)
throw new GameFrameworkException($"Object pool '{FullName}' available-name chain is inconsistent.");
if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i)
throw new GameFrameworkException($"Object pool '{FullName}' available-name link is inconsistent.");
if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name link is inconsistent.");
}
}
}
else
{
if (inUnusedList)
throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in unused list.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list.");
}
if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0)
throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in available chain.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in available chain.");
}
}
}
if (aliveCount != m_TargetMap.Count)
throw new GameFrameworkException($"Object pool '{FullName}' alive count is inconsistent.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' alive count is inconsistent.");
}
int walkUnusedCount = 0;
int current = m_UnusedHead;
@ -605,9 +758,13 @@ namespace AlicizaX.ObjectPool
{
ref var slot = ref m_Slots[current];
if (!slot.IsAlive() || slot.SpawnCount != 0)
throw new GameFrameworkException($"Object pool '{FullName}' unused chain contains invalid slot.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot.");
}
if (slot.PrevUnused != prevUnused)
throw new GameFrameworkException($"Object pool '{FullName}' unused chain linkage is inconsistent.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain linkage is inconsistent.");
}
walkUnusedCount++;
prevUnused = current;
@ -615,7 +772,10 @@ namespace AlicizaX.ObjectPool
}
if (walkUnusedCount != unusedCount)
throw new GameFrameworkException($"Object pool '{FullName}' unused chain count is inconsistent.");
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain count is inconsistent.");
}
#endif
}
private void MarkSlotAvailable(int idx)
@ -677,7 +837,12 @@ namespace AlicizaX.ObjectPool
{
ref var slot = ref m_Slots[idx];
if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0)
throw new GameFrameworkException($"Object pool '{FullName}' available-name chain is inconsistent.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent.");
#endif
return;
}
string objectName = slot.Obj.Name ?? string.Empty;
if (m_AvailableNameTailMap.TryGetValue(objectName, out int tail))
@ -685,14 +850,14 @@ namespace AlicizaX.ObjectPool
m_Slots[tail].NextAvailableByName = idx;
slot.PrevAvailableByName = tail;
slot.NextAvailableByName = -1;
m_AvailableNameTailMap[objectName] = idx;
m_AvailableNameTailMap.AddOrUpdate(objectName, idx);
}
else
{
slot.PrevAvailableByName = -1;
slot.NextAvailableByName = -1;
m_AvailableNameHeadMap[objectName] = idx;
m_AvailableNameTailMap[objectName] = idx;
m_AvailableNameHeadMap.AddOrUpdate(objectName, idx);
m_AvailableNameTailMap.AddOrUpdate(objectName, idx);
}
}
@ -713,14 +878,14 @@ namespace AlicizaX.ObjectPool
if (prev >= 0)
m_Slots[prev].NextAvailableByName = next;
else if (next >= 0)
m_AvailableNameHeadMap[objectName] = next;
m_AvailableNameHeadMap.AddOrUpdate(objectName, next);
else
m_AvailableNameHeadMap.Remove(objectName);
if (next >= 0)
m_Slots[next].PrevAvailableByName = prev;
else if (prev >= 0)
m_AvailableNameTailMap[objectName] = prev;
m_AvailableNameTailMap.AddOrUpdate(objectName, prev);
else
m_AvailableNameTailMap.Remove(objectName);

View File

@ -92,7 +92,13 @@ namespace AlicizaX.ObjectPool
int IObjectPoolServiceDebugView.GetAllObjectPools(bool sort, ObjectPoolBase[] results)
{
if (results == null) throw new GameFrameworkException("Results is invalid.");
if (results == null)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Results is invalid.");
#endif
return 0;
}
List<ObjectPoolBase> source = m_ObjectPoolList;
if (sort)
@ -115,7 +121,12 @@ namespace AlicizaX.ObjectPool
{
var key = new TypeNamePair(typeof(T), options.Name);
if (m_ObjectPools.ContainsKey(key))
throw new GameFrameworkException($"Already exist object pool '{key}'.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Already exist object pool '{key}'.");
#endif
return null;
}
var pool = new ObjectPool<T>(
options.Name ?? string.Empty,
@ -123,7 +134,8 @@ namespace AlicizaX.ObjectPool
options.AutoReleaseInterval ?? DefaultAutoReleaseInterval,
options.Capacity ?? DefaultCapacity,
options.ExpireTime ?? DefaultExpireTime,
options.Priority);
options.Priority,
options.ReleaseStrategy);
m_ObjectPools.Add(key, pool);
m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count);
@ -136,7 +148,12 @@ namespace AlicizaX.ObjectPool
ValidateObjectType(objectType);
var key = new TypeNamePair(objectType, options.Name);
if (m_ObjectPools.ContainsKey(key))
throw new GameFrameworkException($"Already exist object pool '{key}'.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Already exist object pool '{key}'.");
#endif
return null;
}
var poolType = typeof(ObjectPool<>).MakeGenericType(objectType);
var pool = (ObjectPoolBase)Activator.CreateInstance(poolType,
@ -145,7 +162,8 @@ namespace AlicizaX.ObjectPool
options.AutoReleaseInterval ?? DefaultAutoReleaseInterval,
options.Capacity ?? DefaultCapacity,
options.ExpireTime ?? DefaultExpireTime,
options.Priority);
options.Priority,
options.ReleaseStrategy);
m_ObjectPools.Add(key, pool);
m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count);
@ -175,13 +193,25 @@ namespace AlicizaX.ObjectPool
public bool DestroyObjectPool<T>(IObjectPool<T> objectPool) where T : ObjectBase
{
if (objectPool == null) throw new GameFrameworkException("Object pool is invalid.");
if (objectPool == null)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Object pool is invalid.");
#endif
return false;
}
return InternalDestroy(new TypeNamePair(typeof(T), objectPool.Name));
}
public bool DestroyObjectPool(ObjectPoolBase objectPool)
{
if (objectPool == null) throw new GameFrameworkException("Object pool is invalid.");
if (objectPool == null)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Object pool is invalid.");
#endif
return false;
}
return InternalDestroy(new TypeNamePair(objectPool.ObjectType, objectPool.Name));
}
@ -253,9 +283,18 @@ namespace AlicizaX.ObjectPool
private static void ValidateObjectType(Type objectType)
{
if (objectType == null)
throw new GameFrameworkException("Object type is invalid.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Object type is invalid.");
#endif
return;
}
if (!typeof(ObjectBase).IsAssignableFrom(objectType))
throw new GameFrameworkException($"Object type '{objectType.FullName}' is invalid.");
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object type '{objectType.FullName}' is invalid.");
#endif
}
}
private static int ObjectPoolComparer(ObjectPoolBase a, ObjectPoolBase b)

View File

@ -0,0 +1,28 @@
namespace AlicizaX.ObjectPool
{
/// <summary>
/// 对象池释放策略
/// </summary>
public enum ReleaseStrategy
{
/// <summary>
/// LRU (Least Recently Used) - 最近最少使用
/// </summary>
LRU = 0,
/// <summary>
/// LFU (Least Frequently Used) - 最不经常使用
/// </summary>
LFU = 1,
/// <summary>
/// Priority - 基于优先级
/// </summary>
Priority = 2,
/// <summary>
/// Hybrid - 混合策略 (LRU + Priority)
/// </summary>
Hybrid = 3
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 311d5e5b578ed15428d9565ab237becc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,24 @@
using System;
using System.Buffers;
namespace AlicizaX.ObjectPool
{
/// <summary>
/// 数组池管理器,避免频繁分配数组
/// </summary>
internal static class SlotArrayPool<T>
{
private static readonly ArrayPool<T> s_Pool = ArrayPool<T>.Create(256, 50);
public static T[] Rent(int minimumLength)
{
return s_Pool.Rent(minimumLength);
}
public static void Return(T[] array, bool clearArray = false)
{
if (array != null)
s_Pool.Return(array, clearArray);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec27a420ef8eea14fade722558971daa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,183 @@
using System;
using System.Runtime.CompilerServices;
namespace AlicizaX.ObjectPool
{
/// <summary>
/// 字符串键的开放寻址哈希表零GC实现
/// </summary>
internal struct StringOpenHashMap
{
private int[] m_Buckets;
private string[] m_Keys;
private int[] m_Values;
private int[] m_Next;
private int m_Count;
private int m_FreeList;
private int m_Mask;
private int m_AllocCount;
private const int MinCapacity = 8;
public int Count => m_Count;
public StringOpenHashMap(int capacity)
{
int cap = NextPowerOf2(Math.Max(capacity, MinCapacity));
m_Mask = cap - 1;
m_Buckets = new int[cap];
m_Keys = new string[cap];
m_Values = new int[cap];
m_Next = new int[cap];
m_Count = 0;
m_FreeList = 0;
m_AllocCount = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(string key, out int value)
{
if (m_Buckets == null || key == null) { value = -1; return false; }
int hash = key.GetHashCode() & 0x7FFFFFFF;
int i = m_Buckets[hash & m_Mask];
while (i > 0)
{
int idx = i - 1;
if (m_Keys[idx] == key) { value = m_Values[idx]; return true; }
i = m_Next[idx];
}
value = -1;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddOrUpdate(string key, int value)
{
if (key == null) return;
if (m_Count >= ((m_Mask + 1) * 3 >> 2))
Grow();
int hash = key.GetHashCode() & 0x7FFFFFFF;
int bucket = hash & m_Mask;
int i = m_Buckets[bucket];
while (i > 0)
{
int ei = i - 1;
if (m_Keys[ei] == key) { m_Values[ei] = value; return; }
i = m_Next[ei];
}
int idx;
if (m_FreeList > 0)
{
idx = m_FreeList - 1;
m_FreeList = m_Next[idx];
}
else
{
if (m_AllocCount > m_Mask) { Grow(); bucket = hash & m_Mask; }
idx = m_AllocCount++;
}
m_Keys[idx] = key;
m_Values[idx] = value;
m_Next[idx] = m_Buckets[bucket];
m_Buckets[bucket] = idx + 1;
m_Count++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Remove(string key)
{
if (m_Buckets == null || key == null) return false;
int hash = key.GetHashCode() & 0x7FFFFFFF;
int bucket = hash & m_Mask;
int prev = 0;
int i = m_Buckets[bucket];
while (i > 0)
{
int idx = i - 1;
if (m_Keys[idx] == key)
{
if (prev == 0) m_Buckets[bucket] = m_Next[idx];
else m_Next[prev - 1] = m_Next[idx];
m_Keys[idx] = null;
m_Values[idx] = -1;
m_Next[idx] = m_FreeList;
m_FreeList = idx + 1;
m_Count--;
return true;
}
prev = i;
i = m_Next[idx];
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ContainsKey(string key)
{
return TryGetValue(key, out _);
}
public void Clear()
{
if (m_Buckets == null) return;
int cap = m_Mask + 1;
Array.Clear(m_Buckets, 0, cap);
Array.Clear(m_Keys, 0, cap);
Array.Clear(m_Values, 0, cap);
Array.Clear(m_Next, 0, cap);
m_Count = 0;
m_FreeList = 0;
m_AllocCount = 0;
}
private void Grow()
{
int newCap = (m_Mask + 1) << 1;
if (newCap < MinCapacity) newCap = MinCapacity;
int newMask = newCap - 1;
var newBuckets = new int[newCap];
var newKeys = new string[newCap];
var newValues = new int[newCap];
var newNext = new int[newCap];
int newAlloc = 0;
int oldCap = m_Mask + 1;
for (int b = 0; b < oldCap; b++)
{
int i = m_Buckets[b];
while (i > 0)
{
int old = i - 1;
int ni = newAlloc++;
newKeys[ni] = m_Keys[old];
newValues[ni] = m_Values[old];
int hash = newKeys[ni].GetHashCode() & 0x7FFFFFFF;
int nb = hash & newMask;
newNext[ni] = newBuckets[nb];
newBuckets[nb] = ni + 1;
i = m_Next[old];
}
}
m_Buckets = newBuckets;
m_Keys = newKeys;
m_Values = newValues;
m_Next = newNext;
m_Mask = newMask;
m_AllocCount = newAlloc;
m_FreeList = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int NextPowerOf2(int v)
{
v--;
v |= v >> 1; v |= v >> 2; v |= v >> 4;
v |= v >> 8; v |= v >> 16;
return v + 1;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac1ce9399b68c57439691aae9aec01ca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: