对象池的作用

前文引用池篇已经讲过对象池相关作用,这里就不再重复了,GF 中对象池与引用池作用类似,引用池用于普通的 C# 对象,而对象池则一般用于储存 UnityEngine 下的对象(如 Unity 中的 GameObject 对象),具体区别见下文。

对象池的实现

结构

对象池的实现我们可以把他分成 3 部分,上图中从上到下每一行就是一部分,分别是物体信息部分(抽象类 ObjectBase,泛型类 Object, 结构体 ObjectInfo), 对象池部分(抽象类 ObjectPoolBase,接口 IObjectPool,泛型类 ObjectPool,委托 ReleaseObjectFilterCallback),对象池管理器部分(接口 IObjectPoolManager,类 ObjectPoolManager)。其中 Object 和 ObjectPool 是 ObjectPoolManager 的内部私有类。

物体部分

ObjectBase

对象池并没有直接储存目标对象,追溯到代码最下层,发现储存的是 ObjectBase 对象,而 ObjectBase 类型有一个 object 类型的 m_Target 字段,这个字段引用的对象才是我们最终期望的、需要储存的 GameObject 或者继承 MonoBehavior 类对象(当然还可以是其他类型)。也就是说对于每一个我们想要储存的对象,我们都需要另外实现一个继承 ObjectBase 的类,这个类一方面作为目标对象的容器,可避免 GF 与具体业务的耦合,另一方面这个类也包含目标对象的信息状态,包含名字、锁定状态、优先级、自定义释放检查标记、上次使用时间。通过 Initialize 方法可把目标对象传递给 m_Target 字段,通过重写 OnSpawn、OnUnspawn 方法实现对象获取、回收时执行的逻辑。

ObjectBase 实现了 IReference 接口,也就是我们在外部获得其子类时应该从引用池获取。

Object

我们已经知道对象池并不直接储存目标对象,追溯到最下层储存的是 ObjectBase 对象,但 ObjectBase 对象也不是对象池直接储存的对象,只是间接对象。对象池直接储存的是泛型类 Object 的对象,Object 泛型参数约束为 ObjectBase 类型,类中有一个类型为泛型 T 的字段,这个字段会引用对应的 ObjectBase 对象,对象池最终引用关系如上图所示。对象池不直接访问 ObjectBase 对象,而是访问 Object 对象,Object 类内大部分属性是直接返回 ObjectBase 对象的对应属性,除此之外 Object 会记录目标对象的是否正在使用状态以及获取计数。ObjectBase 的 OnSpawn 和 OnUnspawn 通过 Object 的 Spawn 和 UnSpawn 调用。

Object 同样实现了 IReference 接口,但对象池内部已经自行管理,会在注册对象时向引用池获取,外部无需关心,且 Object 是 ObjectPoolManager 的内部私有类,外部并不知晓它的存在。

对象池部分

ObjectPoolBase 与 IObjectPool

ObjectPoolBase 是个抽象类,IObjectPool 是一个泛型接口,从 ObjectPoolBase 和 IObjectPool 的成员可以看到,他们的内容有大部分重叠,IObjectPool 涵盖了 ObjectPoolBase 的绝大部分成员,而对象池类 ObjectPool 类同时继承 ObjectPoolBase 并实现 IObjectPool 类。部分人可能会有疑问,为何设计大部分内容相同的一个抽象类、和一个接口,然后最终只由一个类去继承和实现它们。其实关键点是在于 IObjectPool 是一个泛型接口,而 ObjectPoolBase 不是泛型,当我们需要同时获取多个不同对象池的一些通用数据时,我们可以以 ObjectPoolBase[] 的形式获取到不同的对象池集合,并获取它们各自的名字、数量等数据。而 IObjectPool 则明确清楚某个池子储存的对象的类型,且具有编译时类型安全,所以 Register、Spawn、Unspawn 等需要关心目标物体类型的方法仅在 IObjectPool 接口中声明。我们可以在不同的情况下,以不同的类型持有对象池,如 ObjectPoolManager、以及 UGF 中用以在 Inspector 面板显示对象池数据的 ObjectPoolComponentInspector,都是以 ObjectPoolBase 集合的形式持有对象池,而 UIManager、EntityGroup 中则以 IObjectPool 形式持有,显然前者更适合处理集合通用逻辑,而后者适合处理对具体目标对象的操作。

ObjectPool

  • ObjectPool 类内用两个字段储存着对 Object 的映射关系,分别是 GameFrameworkMultiDictionary > 类型的 m_Objects 和 Dictionary > 类型的 m_ObjectMap。GameFrameworkMultiDictionary 是 GF 内封装的数据结构,与 C# 自身的 Dictionary 类似,不同的是 Dictionary 的 Tkey 与 Tvalue 是一对一的映射关系,而 GameFrameworkMultiDictionary 则是一个 Tkey 对应一个 Tvalue 的集合,是一对多 Tvalue 的关系。其中 m_Objects 为以 Object 的 Name 为 Key,拥有相同 Name 的 Object 集合为 Value。m_ObjectMap 为以以目标对象(ObjectBase 里的 Target)为 Key,Object 为 Value。
  • 通过 Register 接口往对象池注册可用对象,参数类型为继承 ObjectBase 的类,Register 内部会向引用池获取 Objct 对象,并把它加入到 m_Objects 和 m_ObjectMap 中。
  • 属性 AllowMultiSpawn,把对象池分为两种类型,一种是允许对象被多次获取,另一种是不允许。两者区别在于,如果允许对象被多次获取,那么即使一个对象已经处于被使用状态时(即上一次获取后还没归还对象池),仍然可以再次获取,显然一般情况下是不允许这种做法的。在 GF 的资源模块中会使用允许对象被多次获取的对象池来管理资源对象,因为资源对象我们只需要其在内存中存在一份。这个属性会在创建对象池时从参数带入,创建对象池后无法再改变。
  • Spawn 方法接受一个字符串参数,对应 Object 的 Name,若 m_Objects 中存在这个 Key,则取出对应的 Object 集合,并检查其中是否有可用的,若存在可用的就调用 Object 的 SpawnObjectBase 的 Onspawn 完成获取逻辑,最后返回具体的 ObjectBase 的子类。
  • Unspawn 方法接受一个要回收的对象(ObjectBase 中的 Target)参数,方法内部会做一个检查,如果这个对象本来没有通过 Register 方法注册到对象池中,也就是不在字典 m_ObjectMap 中,会抛出错误,若是已注册对象,则会调用 bject 的 UnspawnObjectBase 的 OnUnspawn,完成回收逻辑。
  • GetAllObjectInfos 方法返回 ObjectInfo 结构体数组,包含对象池内所有物体的信息,包括名字、锁定状态、自定义释放检查标记、优先级、使用状态、上次使用时间、获取计数、是否处于使用中状态。
  • 对象池具有自动释放对象的功能,总的来说每过一段时间会调用一次 Release 执行释放逻辑,这个时间由 AutoReleaseInterval 属性决定,每个对象池可以有各自不一样的释放时间间隔。Release 过程会先获取可释放对象序列,然后通过委托 ReleaseObjectFilterCallback 对可释放物体序列进行筛选后,最后仅对筛选后的对象调用 ReleaseObject 进行释放。下面我们看一下相关实现:
public void Release(int toReleaseCount, ReleaseObjectFilterCallback<T> releaseObjectFilterCallback)
{
    if (releaseObjectFilterCallback == null)
    {
        throw new GameFrameworkException("Release object filter callback is invalid.");
    }
 
    if (toReleaseCount < 0)
    {
        toReleaseCount = 0;
    }
 
    DateTime expireTime = DateTime.MinValue;
    if (m_ExpireTime < float.MaxValue)
    {
        expireTime = DateTime.UtcNow.AddSeconds(-m_ExpireTime);
    }
 
    m_AutoReleaseTime = 0f;
    GetCanReleaseObjects(m_CachedCanReleaseObjects);
    List<T> toReleaseObjects = releaseObjectFilterCallback(m_CachedCanReleaseObjects, toReleaseCount, expireTime);
    if (toReleaseObjects == null || toReleaseObjects.Count <= 0)
    {
        return;
    }
 
    foreach (T toReleaseObject in toReleaseObjects)
    {
        ReleaseObject(toReleaseObject);
    }
}

Release 方法就是释放过程的主要逻辑,先调用 GetCanReleaseObjects 获取可释放对象序列,然后用 releaseObjectFilterCallback 对序列进行筛选,最后对筛选后的对象逐个调用 ReleaseObject 进行释放。

private void GetCanReleaseObjects(List<T> results)
{
    if (results == null)
    {
        throw new GameFrameworkException("Results is invalid.");
    }
 
    results.Clear();
    foreach (KeyValuePair<object, Object<T>> objectInMap in m_ObjectMap)
    {
        Object<T> internalObject = objectInMap.Value;
        if (internalObject.IsInUse || internalObject.Locked || !internalObject.CustomCanReleaseFlag)
        {
            continue;
        }
 
        results.Add(internalObject.Peek());
    }
}

GetCanReleaseObjects 方法获取当前可进行释放的对象,会遍历 m_ObjectMap 的 Value 值,对于在处于非使用中状态、非锁定状态、以及自定义释放标记为 True 时,才被认为是可释放对象。

private List<T> DefaultReleaseObjectFilterCallback(List<T> candidateObjects, int toReleaseCount, DateTime expireTime)
{
    m_CachedToReleaseObjects.Clear();
 
    if (expireTime > DateTime.MinValue)
    {
        for (int i = candidateObjects.Count - 1; i >= 0; i--)
        {
            if (candidateObjects[i].LastUseTime <= expireTime)
            {
                m_CachedToReleaseObjects.Add(candidateObjects[i]);
                candidateObjects.RemoveAt(i);
                continue;
            }
        }
 
        toReleaseCount -= m_CachedToReleaseObjects.Count;
    }
 
    for (int i = 0; toReleaseCount > 0 && i < candidateObjects.Count; i++)
    {
        for (int j = i + 1; j < candidateObjects.Count; j++)
        {
            if (candidateObjects[i].Priority > candidateObjects[j].Priority
                || candidateObjects[i].Priority == candidateObjects[j].Priority && candidateObjects[i].LastUseTime > candidateObjects[j].LastUseTime)
            {
                T temp = candidateObjects[i];
                candidateObjects[i] = candidateObjects[j];
                candidateObjects[j] = temp;
            }
        }
 
        m_CachedToReleaseObjects.Add(candidateObjects[i]);
        toReleaseCount--;
    }
 
    return m_CachedToReleaseObjects;
}

DefaultReleaseObjectFilterCallback 是 ReleaseObjectFilterCallback 委托类型方法,这个方法负责从可释放对象序列中进一步选出符合要求的对象,之后再进行释放。DefaultReleaseObjectFilterCallback 是对象池内部的默认实现,我们也可以在构造对象池时传入自定义的方法,根据自定义的逻辑进行筛选。

public bool ReleaseObject(object target)
{
    if (target == null)
    {
        throw new GameFrameworkException("Target is invalid.");
    }
 
    Object<T> internalObject = GetObject(target);
    if (internalObject == null)
    {
        return false;
    }
 
    if (internalObject.IsInUse || internalObject.Locked || !internalObject.CustomCanReleaseFlag)
    {
        return false;
    }
 
    m_Objects.Remove(internalObject.Name, internalObject);
    m_ObjectMap.Remove(internalObject.Peek().Target);
 
    internalObject.Release(false);
    ReferencePool.Release(internalObject);
    return true;
}

ReleaseObject 内部会把对应的 Object 从 m_Objects 和 m_ObjectMap 中移除,调用到 ObjectBase 的 Release(如果目标对象是 GameObject,对 Release 的重写应该是 GameObject.Destroy 相关操作),最后把 ObjectBase 子类对象和 Object 对象归还引用池。(注意这里贴出来的 ReleaseObject 方法参数是应该传入目标物体,也就是 ObjectBase 中的 Target,ReleaseObject 方法还有一个重载是传入 ObjectBase 对象的,上面 Release 方法中调用的是后者,具体看源码)

  • 除了 Release 方法、对象池还提供了 ReleaseAllUnused 该方法会直接释放所有可释放对象,而不经过筛选。
  • 对象池的 ExpireTime 属性决定了对象池里所有对象的过期时间,对象池每过一段间隔时间,就会自动执行释放,根据 DefaultReleaseObjectFilterCallback 的实现,执行释放时会优先获取距对象最后一次使用时间时长大于过期时间的对象。另外如果执行 ReleaseAllUnused,会无视这一过期规则,只要被认为是可释放对象,都会进行回收。
  • SetLocked 方法提供锁定某一对象的功能,即使对象处于未被使用的状态也不会被认为是可释放对象。
  • CustomCanReleaseFlag 提供自定义释放检查标记功能,CustomCanReleaseFlag 是一个虚属性,默认返回 True,也就是对象默认是依赖 IsInUse 这一状态来判断是否使用中,来判断是否能释放,而 IsInUse 这一属性是由 Spawn 与 Unspawn 的计数来判断的,当这种计数方式不适用的情况下,我们可以重写 CustomCanReleaseFlag,自定义逻辑判定是否可释放。

对象池管理器部分

  • ObjectPoolManager 用 Dictionary 类型的 m_ObjectPools 字段储存所有对象池,一个 ObjectBase 子类类型 Type 对象,与创建对象池的传入参数字符串 name 组成一个 TypeNamePair 对象作为唯一 Key,如果我们希望两个对象池储存同样的类型对象,在创建对象池时传入不同的 name 参数即可。
  • CreateSingleSpawnObjectPool 和 CreateMultiSpawnObjectPool 方法创建对象池,分别对应一个对象同时只能被获取一次的对象,以及一个对象能被同时获取多次两种类型的对象池(区别详见上面 ObjectPool 部分的介绍)。这两个创建对象池的方法,GF 提供了非常丰富的重载,可以在创建时指定对象池的名字、自动释放时间间隔、容量、物体过期时间、优先级等。
  • HasObjectPool、GetObjectPool、GetObjectPools、GetAllObjectPools 提供对象池查询功能。
  • Release、ReleaseAllUnused 会对所有对象池执行 Release、ReleaseAllUnused 方法,作用在上文已经说明。
  • DestroyObjectPool 可主动销毁对象池,会回收 ObjectBase、Object 对象到引用池,执行 ObjectBase 的 Release 方法。

示例

以官方 Demo StarForce 的 HPBarComponent 为示例,我们希望把血条用对象池缓存起来,HPBarItem 是控制血条逻辑的类,继承自 MonoBehaviour,为了能实现对 HPBarItem 的缓存,我们先对其定义一个对应的继承 ObjectBase 的类 HPBarItemObject。

HPBarItemObject

public class HPBarItemObject : ObjectBase
{
    public static HPBarItemObject Create(object target)
    {
        HPBarItemObject hpBarItemObject = ReferencePool.Acquire<HPBarItemObject>();
        hpBarItemObject.Initialize(target);
        return hpBarItemObject;
    }
 
    protected override void Release(bool isShutdown)
    {
        HPBarItem hpBarItem = (HPBarItem)Target;
        if (hpBarItem == null)
        {
            return;
        }
 
        Object.Destroy(hpBarItem.gameObject);
    }
}
  • HPBarItemObject 继承自 ObjectBase,我们可以根据需要重写 OnSpawn 和 OnUnspawn,来实现从对象池取出、返回时的逻辑,这里因为我们不需要做任何处理,所以不用重写。而 Release 是 ObjectBase 的抽象方法,我们必须实现 Release 方法,以实现当对象池释放对象时要做的操作,这里对应的操作就是销毁血条的 GameObject 了。
  • 这里另外定义了一个静态的 Create 方法,不要忘记父类 ObjectBase 是实现了 IReference 接口的,我们需要这个类型的对象时应该从引用池取出,而不能另外实例化。这里的 Create 依然是 GF 使用引用池的风格,这样外部需要 HPBarItemObject 只需要调用 Create 即可,Create 需要传入目标对象参数,这里对应的就是 HPBarItem 对象了,然后调用 Initialize 传入 HPBarItem 对象来初始化 HPBarItemObject 对象。
public class HPBarComponent : GameFrameworkComponent
{
    [SerializeField]
    private HPBarItem m_HPBarItemTemplate = null;
 
    [SerializeField]
    private Transform m_HPBarInstanceRoot = null;
 
    [SerializeField]
    private int m_InstancePoolCapacity = 16;
 
    private IObjectPool<HPBarItemObject> m_HPBarItemObjectPool = null;
    private List<HPBarItem> m_ActiveHPBarItems = null;
    private Canvas m_CachedCanvas = null;
 
    private void Start()
    {
        if (m_HPBarInstanceRoot == null)
        {
            Log.Error("You must set HP bar instance root first.");
            return;
        }
 
        m_CachedCanvas = m_HPBarInstanceRoot.GetComponent<Canvas>();
        m_HPBarItemObjectPool = GameEntry.ObjectPool.CreateSingleSpawnObjectPool<HPBarItemObject>("HPBarItem", m_InstancePoolCapacity);
        m_ActiveHPBarItems = new List<HPBarItem>();
    }
 
 
    private void HideHPBar(HPBarItem hpBarItem)
    {
        hpBarItem.Reset();
        m_ActiveHPBarItems.Remove(hpBarItem);
        m_HPBarItemObjectPool.Unspawn(hpBarItem);
    }
 
 
    private HPBarItem CreateHPBarItem(Entity entity)
    {
        HPBarItem hpBarItem = null;
        HPBarItemObject hpBarItemObject = m_HPBarItemObjectPool.Spawn();
        if (hpBarItemObject != null)
        {
            hpBarItem = (HPBarItem)hpBarItemObject.Target;
        }
        else
        {
            hpBarItem = Instantiate(m_HPBarItemTemplate);
            Transform transform = hpBarItem.GetComponent<Transform>();
            transform.SetParent(m_HPBarInstanceRoot);
            transform.localScale = Vector3.one;
            m_HPBarItemObjectPool.Register(HPBarItemObject.Create(hpBarItem), true);
        }
 
        return hpBarItem;
    }
}

上面是 HPBarComponent 的部分代码,为了能更容易注意到关键代码,已经把对象池无关部分代码进行删减。

  • 在 Start 方法中用 CreateSingleSpawnObjectPool 方法创建对象池,传入对象池名字、对象池容量作为参数,并且用 IObjectPool 类型字段来引用着对象池(注意泛型参数不是目标对象 HPBarItem 类,而是为其另外定义的继承自 ObjectBase 的 HPBarItemObject 类)。
  • CreateHPBarItem 方法中,else 分支中便是往对象池注册对象的逻辑,我们先通过 Unity 的 API 实例化出 HpBarItem 目标对象,并进行有必要的初始化,再以此为参数创建 HPBarItemObject 对象,最后调用 Register 方法把 HPBarItemObject 注册到对象池中。这里的 Register 是有第二个参数的,类型为 bool,因为对象池的对象是外部实例化后才注册进去,而不是在池子内部实例化的,此时对象池并不知道这个被注册进来的对象是否处于被使用中的状态,如果注册时就立马使用这个对象,这里参数应该传入 true,如果目前暂时不用,稍后再从对象池通过 Spawn 取出,就应该传入 false。
  • HideHPBar 中,当不需要对象时,调用对象池的 Unspawn 方法向池子归还对象,Unspawn 有两个重载版本,我们传入 HPBarItem 或 HPBarItemObject 对象都可以。

Inspector 面板

Inspector 面板可以在运行时观察所有对象池的实时情况,包括对象池的各项属性以及池子里所有物体的 ObjectInfo 数据。

引用池与对象池的区别

  1. 引用池从池子内部通过默认构造方法创建对象,只适合普通的 C# 对象。对象池是在外部自行创建对象后再注册进去,能用于必须通过 Unity API 才能实例化的对象。
  2. 引用池仅提供 Clear 接口来清除对象状态,在移除对象时没有任何额外处理,仅仅是去掉引用,适用于受 GC 管理的类型。而对象池提供 OnSpawn,OnUnspawn 两个操作,且在移除对象时,提供 Release 接口,对于 Unity 中的 GameObject 需要在 Release 写上 Destroy(gameObject) 的逻辑才能销毁。
  3. 对象池提供自行释放的机制,可指定每个池子自动释放周期、物体过期时长、池子容量,并在可一定程度上自定义每个池子的释放策略。引用池没有以上机制,仅可通过 Remove 接口主动移除对象。
  4. 对象池提供锁定物体、自定义释放标记功能,可进一步定制释放策略。

知道以上的功能区别,相信大家对哪个类型用引用池还是对象池应该有个明确的认识了。

思考

同一个对象池中,为什么还要以 Name 区分对象集合,应用场景是什么

Object 中,m_Objects 字段以 name 来区分不同的 Object 集合,而 Spawn 方法可以获取指定 name 的对象。
一般来说同一个对象池中,我们一般储存同一类型的东西,也就是 ObjectPool 的泛型参数的类型。什么情况下同一个类型的对象还要区分?
参考官方 Demo StarForce 中的陨石对象池,虽然他们都是同一个类型,具有相同的逻辑,但他们可能有不一样的外型。我们把外型不同的陨石做成单独 prefab,并在这些 prefab 上挂上相同的脚本,最后以他们的资源路径名字作为 Name,则可在一个对象池中对不同外形的陨石进行区分,以实现向一个对象池取不同外型的陨石的需求。

在同一个对象池中以 Name 区分对象,与用多个对象池储存不同 Name 的对象有什么区别

主要区别就在于一个对象池执行同一个释放逻辑,而多个对象池是各自执行各自的释放逻辑。继续以上面的陨石为例子,我们一共有 3 种陨石,我希望储存陨石的对象池总容量是 60,我们随机去生成不同种类的陨石,如果随机结果不均匀,最终池子里可能有种类一 40 个,种类二 15 个,种类三 5 个,在我们把他们放在同一对象池下管理情况下,这没有什么问题,无论怎样它都很好地以总数量为 60 个的策略去管理。但如果我们把不同外形的陨石分到不同的对象池去管理,我们很难去动态调整 3 个池子的容量平衡,以达到总数量为 60 的策略。

Lock 的使用场景

当我们希望某个物体长期不使用都不会被释放,被需要时可以快速地响应时,我们可以使用 SetLocked 把这个物体锁定。例如 MMO 游戏的主界面,上面有非常大量的按钮入口,特效,各种各样的信息,当玩家打开一些界面隐藏了主界面,且过了比较长的时间,我们也不希望主界面被销毁,因为主界面重新加载的耗时非常感人,这时候就可以把主界面锁定,避免重新加载,以提高游戏体验。

CustomCanReleaseFlag 使用场景

上文说过,当获取计数不适用于判断是否被使用中的标准时,我们可以通过重写 CustomCanReleaseFlag 属性来实现自定义的释放标记。Unity 中的资源就是其中的例子,除了我们主动去取某个资源时,这个资源也可能会作为其他资源的依赖项被使用,这样我们即使归还了上次的主动获取,我们也不能确定这个资源是否已经没有被依赖,不能认定为可以被释放,所以需要重写 CustomCanReleaseFlag 加上依赖引用数的判断,详见 GF 中 Resource 模块的 AssetObject、ResourceObject。

为什么既有引用池又有对象池,全部用对象池不是就可以满足需求了吗

根据区别我们可以看出,引用池适合更 “轻” 的对象,而对象池适合更 “重” 的对象,把一个对象注册进对象池,还需要用到 ObjectBase、Object 对象来包装他,这对于一个轻量级对象来说未免代价有点大,所以 “轻” 对象还是另外放到引用池比较合适。

最后

GameFramework 解析 系列目录:GameFramework 解析:开篇

个人原创,未经授权,谢绝转载!