【Unity】官方对象池的使用 UnityEngine.Pool

前言

Unity的基础功能残缺得令人发指,官方对象池也是近年才补上的。

概览

使用UnityEngine.Pool引入对象池。

重点在于三个关键类:CollectionPool<TCollection,TItem>,IObjectPool<T>,PooledObject<T>。需要注意的是,官方提供的所有池子都不是线程安全的

CollectionPool<TCollection,TItem>

这是一个用于缓存ICollection的池子,不需要创建实例,只需要使用静态方法Get和Release来获取和释放ICollection对象。以此可以减少频繁的容器创建带来的压力。

例如,若我暂时使用到了List<int>,无需手动创建List<int>对象,仅需要如此做:

//使用静态方法获取List<int>实例
var listInt = CollectionPool<List<int>, int>.Get();
//使用
...
//使用静态方法释放
CollectionPool<List<int>, int>.Release(listInt);

当然,对于常见的容器,Unity也给出了相应的变种,省去了手打类型名的麻烦。

例如,ListPool<T>继承了CollectionPool<List<T>, T>,所以上面代码可以简化成:

//使用静态方法获取List<int>实例
var listInt = ListPool<int>.Get();
//使用
...
//使用静态方法释放
ListPool<int>.Release(listInt);

类似的,还有DictionaryPool<TKey,TValue>,HashSetPool<T>可用于缓存字典和哈希散列。

IObjectPool<T>

对象池的接口,定义如下:

    public interface IObjectPool<T> where T : class
    {
        //池中有多少个对象
        int CountInactive { get; }
        //清除
        void Clear();
        //获取
        T Get();
        //便于释放的获取方式
        PooledObject<T> Get(out T v);
        //释放
        void Release(T element);
    }

其更接近于传统意义上的对象池,需要手动创建对象池的实例,用于缓存引用类型(class)。

官方提供了两个实现类,分别是ObjectPool<T>和LinkedPool<T>。其区别在于ObjectPool内部使用栈(Stack)实现,而LinkedPool内部使用LinkedList实现。

两者实际使用区别不大,LinkedPool虽然使用LinkedList实现,但不能自由插入和删除,同样以Get和Release进行类似于栈的操作。

在构造函数方面,因为采用的存储结构不同,在容量参数上有一点区别,ObjectPool多了一个默认容量的参数。

public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000);

public LinkedPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int maxSize = 10000);

这里比较重要的参数有:

Func<T> createFunc,指示池子如何创建一个实例。当使用Get从池子里获取,但池子里可用的实例数量不足时,就会执行此委托来创建一个新的实例。例如对于一般对象,可以使用()=>new T()创建,而对于预制体对象,可以使用Instantiate()创建。

Action<T> actionOnGet,当使用Get获取实例时,对实例进行的操作。例如对于GameObject而言,可以设为在获取时令其活跃(SetActive(true))。

Action<T> actionOnRelease,当使用Release释放实例时,对实例进行的操作。例如对于GameObject而言,可以设为在获取时令其隐藏(SetActive(false))。

Action<T> actionOnDestroy,当对象池被清除(Clear)或释放(Dispose)时,对实例进行的操作。例如使用Destroy销毁Mono实例。

bool collectionCheck,在释放实例入池时,检查其是否已经在池子里。开启时会有一定的性能开销(用于查询和比较实例是否相同),但可以避免同一实例被释放两次引发的未知错误。

如果不想手动创建对象池,官方也贴心地提供了类似于前面CollectionPool的静态实现版本:GenericPool<T>和

UnsafeGenericPool<T>,使用静态方法Get和Release来创建和释放实例对象。

//截取自官方文档
class MyClass
{
    public int someValue;
    public string someString;
}

void GetPooled()
{
    //获取实例
    var instance = UnsafeGenericPool<MyClass>.Get();
    //释放实例
    UnsafeGenericPool<MyClass>.Release(instance);
}

两者区别在于,GenericPool开启了collectionCheck重复检查,会在检查过程中不可避免产生一些垃圾;而UnsafeGenericPool关闭了重复检查,不产生垃圾。

PooledObject<T>

若从对象池里获取一个实例,使用后应当释放回相应对象池。前面所有提到的池子,都有一个PooledObject<T> Get(out T value)的重载,PooledObject便是用来辅助释放的结构。

PooledObject实现了IDisposable接口,当调用其Dispose方法时,会自动完成对应实例到对应池子的回收。

因此,可以使用如下的结构来完成创建和释放:

//此处重载后的Get返回类型是PooledObject<List<int>>
//因为其实现了IDisposable接口,可以使用using语法释放
using(ListPool<int>.Get(out var list))
{
    list.AddRange(new int[] { 1, 2, 3, 4 });
    list.ForEach(i => Debug.Log(i));
}

总结

尽管省去了一些造轮子的功夫,但实际使用感觉还是差点东西。

例如回调必须也只能在对象池构造时完成,缺少带有配置参数的Get方法,以及缺少可能用到的遍历对象池的方法。

而官方也是一如既往地喜欢把路堵死,重要的字段全是private,例如继承ObjectPool写一个子类,子类的权限和外界竟然别无二致。还是得自己造点轮子,能够适应项目才是最重要的。

© 版权声明
THE END
喜欢就点个赞吧
点赞3 分享
评论 共2条

请登录后发表评论

      • Acoloco的头像-水波萌Acoloco等级-LV4-水波萌作者0