【为什么要用对象池】

有些对象需要在程序运行或游戏过程中重复的创建销毁,例如子弹、怪和粒子等。每次创建要分配内存,而这个对象生命周期很短,对象很快被销毁,内存要被回收,这会增大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

【参考】

一个广为人知但鲜有人用的技巧:对象池-InfoQ

对象池模式 · Optimization Patterns · 游戏设计模式 (tkchu.me)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐