Unity中的资源管理-对象池技术(2)

10 篇文章 15 订阅
3 篇文章 0 订阅

本文分享Unity中的资源管理-对象池技术(2)

在上一篇文章中, 我们一起学习了普通类的对象池, 这种实现有一定的特点和适用范围:

  • 只能存放相同类型的对象
  • 所有对象都一视同仁, 在产生和回收后都需要额外的操作, 比如产生后需要初始化, 回收后需要重置信息

今天要介绍一种更加通用和灵活的实现方式: 这种对象池只负责存取对象, 而这些对象不拘泥类型且不需要额外的操作就能使用和回收.

简单说就是上一个对象池实现特点的反面.

对象池? 容器?

可能有些同学会有疑问, 如果只是需要一个存取对象的容器, 那么我用ArrayList, List, 或者Dictionary不就行了么, 为什么还需要单独做一个对象池呢?

其实这些容器都可以看做是这种对象池的某种实现, 至于可以存放各种类型对象, 我们只需要让这些容器存放的是object类型即可. 也就是说你可以把这些容器都称为对象池. 对象池的观点其实很宽泛, 只要能存放大量对象, 并提供对应的产生和回收接口即可.

今天要介绍的实现方式就是这样一种容器, 能够存放各种类型的对象, 而且可以及其快速的存取.

大家肯定第一个就能想到, 支持及其快速存取的就是数组, 存取都是O(1). 没错, 这种对象池就是基于数组, 是在一定程度上优化了数组的缺点的实现.

数组的缺点及其优化方式有:

  • 容量大小固定
    • 通过动态扩容和缩容解决
    • List就是在数组的基础上进行这种优化的容器
  • 只能存放同一个类型的对象
    • 通过存放object类型的对象解决
    • ArrayList就是在数组的基础上进行这种优化的容器
    • 但是ArrayList在删除元素时会移动删除位置之后的所有的元素
  • 只能通过整型索引对象
    • 通过将其它类型的索引转换为整型解决
    • Dictionary就是通过将其它类型的索引通过计算hash值转换为整型索引的容器

总之, 数组是一个非常基础的容器, 但是有各种各样的缺陷, 但是我们可以通过解决各种缺陷来产生新的容器, 这些容器继承了数组的快速存取而克服了部分缺陷. 我们日常中使用的大部分容器在底层都是通过数组实现的, 比如ArrayList, Stack, Queue, List, Dictionary, HashMap等等.

今天要介绍的对象池和ArrayList的实现有些类似, 但是克服了其需要在删除元素时移动元素的缺点. 其支持的特性如下:

  • 支持极其快速存取对象, 且不用移动对象
  • 支持存放任意类型的对象
  • 支持动态扩容缩容
  • 使用整型索引对象

这就是Xlua中对象池的实现, Xlua使用这种对象池管理所有lua引用的C#对象.

实现原理

首先其底层容器是一个数组, 因为要记录位置信息, 所以需要的是一个二级结构, 这个结构存放了下一个可用位置的信息, 并且持有了真正需要存取的对象.

const int LIST_END = -1;
const int ALLOCED = -2;

// 位置插槽, 用于描述位置信息, 用类也可以
struct Slot
{
    // 如果是正值代表下一个可用位置, 如果为-2(ALLOCED)代表当前位置已被占用, -1代表这个位置是最后一个可用位置
    public int next;
    
    // 持有对象
    public object obj;
}

// 底层容器
private Slot[] list = new Slot[512];

然后, 我们需要一个字段来记录当前使用的插槽数量: private int count = 0.

再然后, 我们需要一个链表将所有可用位置串连, 当然, 我们不是使用真正的链表, 而是使用Slotnext字段结合一个表头索引来串连: private int freelist = LIST_END. 一旦这个索引指向了一个确切的位置, 那么代表我们拥有了至少一个可用位置, 至于有没有更多可用的位置, 那么需要查看该位置的next字段是否为正值. 这里纯粹使用语言来描述比较让人迷惑, 大家只需要有一个简单的印象就行了. 总之, 表头索引指向可用位置, 通过可用位置的next串连下一个可用位置.

最后, 我们来描述详细的算法:

  • 初始状态下, freelist = LIST_END, 代表现在没有可用位置.
  • 当添加一个对象时, 发现没有可用位置, 那么向对象池申请一个新的位置: index = count++, 也就是说可用位置为0, 并且将count++
  • 使用该位置存储对象, 并将next字段置为已被占用的状态: list[index].obj = obj; list[index].next = ALLOCED, 并返回索引.
  • 此时池中有一个对象(位置index = 0; count = 1), 可用位置依然为0(freelist = LIST_END)
  • 当删除一个对象时, 通过对象的索引查找到位置并情况位置持有的对象: list[index].obj = null
  • 将当前位置的置为可用状态, 且按照头插法插入可用链表: list[index].next = freelist; freelist = index;
  • 最后返回已经删除的对象
  • 此时池中没有对象, 有一个可用位置(位置index = 0; count = 1; freelist = 0; list[0].next = -1)
  • 再次添加一个对象, 发现有可用位置(freelist = 0), 直接使用该位置, 并在头部将位置从可用位置链表中移除: list[0].obj = obj; freelist = list[0].next;, 最后将位置的状态设置为已占用: list[0].next = ALLOCED;
  • 最后返回索引

以上就是整个算法的核心实现了. 这个算法十分巧妙, 但是也让人特别迷惑, 建议对链表不熟悉的同学先温习一下链表的基础知识, 看的头晕就不要吐槽作者了哟.

下面贴出添加和删除还有扩容部分的完整代码, 这里作者做了一些修改以便更容易理解.

// 申请一个新的大小为原始数组两倍大小的数组作为底层容器, 并被将原始数组的内容复制到新的数组
void extend_capacity()
{
    Slot[] new_list = new Slot[list.Length * 2];
    for (int i = 0; i < list.Length; i++)
    {
        new_list[i] = list[i];
    }
    
    list = new_list;
}

public int Add(object obj)
{
    // 没有可用位置, 向对象池申请
    if (freelist == LIST_END) {
        if (count == list.Length)
        {
            // 对象池中已经没有更多位置, 需要扩容
            extend_capacity();
        }

        // 申请一个新的位置
        var newSlotIndex = count++;
        ref var slot = ref list[newSlotIndex]; // 因为struct是值类型, 需要引用

        // 使用该位置并置占用状态
        slot.obj = obj;
        slot.next = ALLOCED;
        return newSlotIndex;
    }

    // 从头部取出第一个可用位置, 将该位置从可用链表中移除 
    var freeSlotIndex = freelist;
    ref var freeSlot = ref list[freeSlotIndex];
    freelist = freeSlot.next;

    // 使用该位置并置占用状态
    freeSlot.obj = obj;
    freeSlot.next = ALLOCED;
    return freeSlotIndex;
}

public object Remove(int index)
{
    if (index < 0 || index >= count || list[index].next != ALLOCED)
        return null;

    // 找到位置, 取出对象并清空
    ref var slot = ref list[index];
    object o = slot.obj;
    slot.obj = null;

    // 使用头插法将位置插入可用链表
    slot.next = freelist;
    freelist = index;

    return o;
}

注释已经写的比较清晰明了了, 相信只要熟悉链表知识的同学基本毫无压力.

这里总结一下, count代表已经申请的位置数量, 但是不代表这些位置都被占用了, 只有freelist指向表尾时才代表无可用位置, 需要再次申请. 如果需要知道对象池中已经存在的元素个数, 可以通过count - Length(freelist), 当然, 也可以添加字段来记录数量就行了.

最后再次感叹下这个设计的巧妙, 总之是学到了, 作者君又变强了(当然头发依然茂密)!

好啦, 今天的内容就是这样, 希望对大家有所帮助.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拂面清风三点水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值