【为什么要用对象池】
有些对象需要在程序运行或游戏过程中重复的创建销毁,例如子弹、怪和粒子等。每次创建要分配内存,而这个对象生命周期很短,对象很快被销毁,内存要被回收,这会增大GC的压力,同时也会造成内存碎片。使用对象池可以解决这些问题。
对象池预先初始化一系列可重用的对象,循环利用这些对象,有利于提高程序性能和内存使用率。
【相关概念】
- 池的大小:初始化n个对象,那么池的大小就是n,具体n取值多少依据具体情况而定
- 回收模式:指如何在需要的时候从对象池中取出对象,在不需要的时候将对象放回对象池
- 借用:将对象从对象池中借用出来,借用后在对象池中找不到对象的引用。借用者不需要的时候将对象返回给对象池,借用者不再持有对象的引用。这种方式实现起来简单,大多数对象池都是用这种方实现,本文也采用这种方式实现。
- 引用计数:用于同时有多个借用者访问同一个对象,只有当所有的借用者都释放了对象引用时,对象才可以被回收。每个对象都持有一个内部计数器和一个指向池的引用。当计数器为 0 时,对象就会返回池中。也即对象需要额外挂载一个脚本。
- 分配方式:指对象池内的没有可用对象时,有新的请求时,该如何返回一个对象,常见的策略有:
- 拒绝请求:告知池内没有对象了,拒绝请求,返回空对象
- 强制回收:强行回收一个已经借出的对象
- 增大池子:这种方式使用最多,什么时候增大池子呢?有三种触发方式
- 空池触发:对象池内没有可用对象时,创建一个新对象
- 水位线触发:空池触发的缺点是,请求对象时可能会因为执行对象分配而中断。为了避免这种情况,可以使用水位线触发。当从池中可用对象小于某个阈值,例如池子大小的十分之一,就触发分配过程。
- Lease/Return速度:大多数时候,水位线触发已经足够,但有时候可能会需要更高的精度。在这种情况下,可以使用lease和return速度。例如,如果池中有100个对象,每秒有20个对象被取走,但只有10个对象返回,那么9秒后池就空了。开发者可以使用这种信息,提前做好对象分配计划。
- 增长策略:指如何增大池子
- 固定增长:每次增长一个或几个
- 快速增长:根据池子大小的百分比增长
- 初始化和重置对象:取出对象后,需要对对象做初始化,可以选择在对象池内初始化,可以在借用者获取对象后自己初始化。前者的好处是对象池可以完全封装管理对象,但需要提供很多不同的初始化函数,因为不同的对象初始化的参数不一样。后者更简单的一些,需要记得自己初始化对象,并在返给对象池时重置对象,返回时注意释放引用。
- 对象内存大小固定:必须保证借用者在使用对象时不会引起对象内存大小的改变,否则新增内存将覆盖到下一个对象的内存中
- 借出对象列表:如果需要查找借出的对象,需要创建一个借出对象的列表
下文的代码实现了能存放不同对象的对象池:
【Unity实现】
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class ObjectPools : MonoBehaviour
{
public static ObjectPools Instance;
[Serializable]//可序列化的,能在Inspector面板上看到,
public class PoolInfo//对象池的信息
{
public string poolName;//对象池的名字
public GameObject prefab;//要实例化的prefab
[NonSerialized]
public int poolSize;//当前对象池的可容纳的最大对象
public int fixedSize;//初始化时固定的大小
}
public PoolInfo[] poolInfos;
//用队列,先进先出,循环利用每个对象,不用栈
private Dictionary<PoolInfo, Queue<GameObject>> allObjects = new Dictionary<PoolInfo, Queue<GameObject>>();
//每个借出的对象属于哪个对象池
private Dictionary<GameObject, PoolInfo> lentObjects = new Dictionary<GameObject, PoolInfo>();
private Dictionary<string, PoolInfo> nameToPool = new Dictionary<string, PoolInfo>();
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
PoolInit();
}
/// <summary>
/// 对象池初始化
/// </summary>
public void PoolInit()
{
foreach (PoolInfo item in poolInfos)
{
AddObjPool(item);
}
}
/// <summary>
/// 借出对象
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public GameObject LendObject(string name)
{
GameObject go = null;
PoolInfo poolInfo;
if (nameToPool.TryGetValue(name, out poolInfo))
{
var queue = allObjects[poolInfo];
if (queue.Count > 0)
{
go = queue.Dequeue();
}
else//空池触发
{
go = Instantiate(poolInfo.prefab, transform);
poolInfo.poolSize++;
}
lentObjects.Add(go, poolInfo);
go.SetActive(true);
return go;
}
else
{
Debug.LogWarning("该对象不存在:" + name);
}
return go;
}
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go"></param>
public void Recycle(GameObject go)
{
if (go == null)
{
Debug.LogWarning("不是对象池中的对象");
return;
}
PoolInfo poolInfo = null;
if (lentObjects.TryGetValue(go, out poolInfo))
{
lentObjects.Remove(go);
go.SetActive(false);
allObjects[poolInfo].Enqueue(go);
}
}
/// <summary>
/// 清空所有对象池
/// </summary>
public void ClearPool()
{
lentObjects.Clear();
allObjects.Clear();
nameToPool.Clear();
}
/// <summary>
/// 添加新的对象池
/// </summary>
/// <param name="poolInfo"></param>
public void AddObjPool(PoolInfo poolInfo)
{
var queue = new Queue<GameObject>(poolInfo.fixedSize);//初始化队列大小,要直接把poolInfo.fixedSize传入
if (poolInfo.prefab == null)
{
Debug.LogError("欲实例化的物体为空:" + poolInfo.poolName);
return;
}
if (string.IsNullOrEmpty(poolInfo.poolName))
poolInfo.poolName = poolInfo.prefab.name;
if (nameToPool.ContainsKey(poolInfo.poolName))
{
Debug.LogWarning("该对象池已存在:"+poolInfo.poolName);
return;
}
poolInfo.poolSize = poolInfo.fixedSize;
allObjects.Add(poolInfo, queue);
for (int i = 0; i < poolInfo.fixedSize; i++)
{
GameObject go = Instantiate(poolInfo.prefab, transform);
go.SetActive(false);
queue.Enqueue(go);
}
nameToPool.Add(poolInfo.poolName, poolInfo);
}
/// <summary>
/// 移出某个对象池
/// </summary>
/// <param name="poolName"></param>
public void RemoveObjPool(string poolName)
{
if (nameToPool.ContainsKey(poolName))
{
Debug.LogWarning("该对象池不存在:"+poolName);
return;
}
PoolInfo poolInfo = nameToPool[poolName];
nameToPool.Remove(poolName);
allObjects.Remove(poolInfo);
for (int i = lentObjects.Count-1; i > 0; i--)
{
if (lentObjects.ElementAt(i).Value == poolInfo)
lentObjects.Remove(lentObjects.ElementAt(i).Key);
}
}
}
【Lua实现】
local Objectpool = {}
---@param poolInfos table 对象池初始化的信息
function Objectpool:PoolInit(poolInfos)
self.allObjects = {}
self.lentObjects = {}
self.nameToPool = {}
for _,v in pairs(poolInfos) do
self:AddObjPool(v)
end
end
---@param poolName string 对象池的名字
---@return GameObject 从对象池中借出的对象
function Objectpool:LendObject(poolName)
if self.nameToPool[poolName] == nil then
print("不存在该对象池")
return nil
end
local poolInfo = self.nameToPool[poolName]
local pool = self.allObjects[poolInfo]
local obj = nil
if #pool > 0 then
obj = pool:pop()
else --空池触发,每次增加1
obj = CS.UnityEngine.Object.Instantiate(poolInfo.obj)
poolInfo.poolsize = poolInfo.poolsize+1
end
self.lentObjects[obj] = poolInfo
return obj
end
---
---@param obj GameObject 回收的物体
function Objectpool:Recycle(obj)
if obj == nil then
print("回收的物体为空")
return
end
if self.lentObjects[obj] == nil then
print("不是对象池中的物体:",obj.name)
return
end
self.allObjects[self.lentObjects[obj]]:push(obj)
self.lentObjects[obj] = nil
end
---
---@param poolInfo table 添加新的对象池
function Objectpool:AddObjPool(poolInfo)
local stack = Array.New()
if poolInfo.obj == nil then
print("实例化对象为空")
return
end
if poolInfo.poolName == nil then
print("对象池名字为空")
return
end
local key = poolInfo.poolName
if self.nameToPool[key] ~= nil then
print("该对象池已存在:",key)
return
end
poolInfo.poolsize = poolInfo.fixedSize
self.allObjects[poolInfo] = stack
for i = 1, poolInfo.fixedSize do
local obj = CS.UnityEngine.Object.Instantiate(poolInfo.obj)
stack:push(obj)
end
self.nameToPool[key] = poolInfo
end
---
---@param poolName string 移除某个对象池
function Objectpool:RemoveObjPool(poolName)
if self.nameToPool[poolName] == nil then
print("该对象池不存在:",poolName)
return
end
local poolInfo = self.nameToPool[poolName]
self.allObjects[poolInfo] = nil
self.nameToPool[poolName] = nil
for k,v in pairs(self.lentObjects) do
if v == poolInfo then
self.lentObjects[k] = nil
end
end
end
_G.ObjectPool = Objectpool