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

接下来几天, 作者会按照自己的理解写几篇关于Unity中的资源管理相关的文章.

大概会涉及到:

  • 对象池: 分为普通类GameObject(主要是预制)的对象池
  • 引用计数技术
  • Unity中的资源基本概念, 分类, 基本使用: 包含Resources, AssetDatabase
  • Unity中的Ab包: 包含Ab包的介绍, 生成, 加载和卸载
  • Unity中使用Profiler进行内存分析
  • 一整套资源管理方案

今天分享对象池技术的第一部分: 普通类的对象池.

什么是对象池以及为什么需要对象池

在正式开始之前, 我们需要搞明白为什么需要对象池.

对象的实例化可能是一个比较繁重的工作, 比如预制的实例化, 需要将在背后做很多事情, 如加载其依赖的资源, 实例化这些资源, 给资源对象赋值, 给预制对象赋值, 最终得到预制的实例化对象. 针对同一个预制, 如果需要多次使用和销毁其实例化对象, 会造成资源浪费和卡顿, 我们可以在不使用的时候将其实例化对象保存下来而不是直接销毁, 在下次使用时直接给予缓存的对象而不需要进行实例化, 这样可以充分利用资源, 也可以加速进程.

简单的说对象池就是将不需要使用的多个对象存下来, 下次需要使用时直接给予而不再进行实例化的技术.

在我们游戏开发中最常见的对象池技术的实例就是子弹的使用.

想象一下, 如果不使用对象池, 我们就会频繁的创建和销毁对象, 这对性能是很大的影响.

对象池是一个典型的空间换时间的技术, 为了玩家流畅的体验, 我们一般会在进入场景前预加载和预创建好一定数量的对象存放在对象池中, 然后在游戏过程中直接拿来使用即可.

简单的对象池实现

首先, 我们先实现一个满足基本需求的简单对象池: 可以产生对象, 回收对象, 销毁对象.

public class SimpleObjectPool<T> where T : class, new() {
    protected readonly Stack<T> m_ObjPool;

    protected int m_MaxCount;
    protected int m_OnceReleaseCount;

    public SimpleObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        m_ObjPool = new Stack<T>(initCapacity);

        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;
    }

    public T Spawn() {
        var obj = m_ObjPool.Count <= 0 ? CreateObj() : m_ObjPool.Pop();
        return obj;
    }

    public void Recycle(T obj) {
        if (m_ObjPool.Count >= m_MaxCount) {
            ReleaseOverflowObj();
        }

        m_ObjPool.Push(obj);
    }

    private T CreateObj() {
        var obj = new T();
        return obj;
    }

    /// <summary>
    /// 移除多余对象
    /// </summary>
    private void ReleaseOverflowObj() {
        Debug.Log("[ReleaseOverflowObj] 已达最大回收数量: " + m_ObjPool.Count);

        var removeCount = Math.Min(m_OnceReleaseCount, m_ObjPool.Count);
        while(--removeCount >= 0) {
            var obj = m_ObjPool.Pop();
        }

        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }
}

我们使用泛型来创建对象池, 这样可以很好的兼容大部分类.

第一行public class SimpleObjectPool<T> where T : class, new(), 其中where T : class, new()代表对于容纳的结构要有两个约束: 结构T必须是类(class), 并且拥有无参构造函数(new()).

我们使用栈来当做对象的容器, 因为我们会频繁的添加和删除, 而栈对于这两个操作的复杂度都是O(1)级别的.

常见的另一个实现是使用数组, 数组的每个位置代表一个插槽, 待回收的对象插入插槽, 并返回索引, 需要使用时依靠索引来获取, Xlua的对象池就是使用这种方式.

SpawnRecycle接口分别代表产生和回收对象, 逻辑比较简单.

我们的实现还有一个最大的容量限制, 达到最大容量后才会销毁配置数量的对象.

我们需要更多

上面的实现在一些简单的场景是够用了, 但是在日常开发中我们经常需要知道对象的生成和销毁时机, 比如在真正销毁时做一些清理操作.

下面我们添加一些新的功能: 在对象的产生, 回收, 销毁时通知调用方, 并且可以根据调用方提供的方式来创建对象.

public class ObjectPool<T> : IDisposable where T : class, new() {
    public delegate T NewObj();

    protected readonly Stack<T> m_ObjPool;
    protected int m_MaxCount;
    protected int m_OnceReleaseCount;

    protected UnityAction<T> m_BeforeSpawn, m_AfterRecycle, m_AfterRelease;
    protected NewObj m_NewObjDelegate;

    public ObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10,
                      UnityAction<T> beforeSpawn = null, UnityAction<T> afterRecycle = null, UnityAction<T> afterRelease = null, 
                      NewObj newObj = null) {

        m_ObjPool = new Stack<T>(initCapacity);

        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;

        m_BeforeSpawn = beforeSpawn;
        m_AfterRecycle = afterRecycle;
        m_AfterRelease = afterRelease;
        m_NewObjDelegate = newObj;
    }

    public void Dispose() {
        if (m_AfterRelease != null) {
            var array = m_ObjPool.ToArray();
            foreach(var obj in array) {
                m_AfterRelease(obj);
            }
        }

        m_ObjPool.Clear();

        m_BeforeSpawn = m_AfterRecycle = m_AfterRelease = null;
        m_NewObjDelegate = null;
    }

    private T CreateObj() {
        var obj = m_NewObjDelegate != null ? m_NewObjDelegate() : new T();
        return obj;
    }

    /// <summary>
    /// 移除多余对象
    /// </summary>
    private void ReleaseOverflowObj() {
        Debug.Log("[ReleaseOverflowObj] 已达最大回收数量: " + m_ObjPool.Count);

        var removeCount = Math.Min(m_OnceReleaseCount, m_ObjPool.Count);
        while(--removeCount >= 0) {
            var obj = m_ObjPool.Pop();
            m_AfterRelease?.Invoke(obj);
        }

        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }

    public T Spawn() {
        var obj = m_ObjPool.Count <= 0 ? CreateObj() : m_ObjPool.Pop();
        m_BeforeSpawn?.Invoke(obj);

        return obj;
    }

    public void Recycle(T obj) {
        if (m_ObjPool.Count >= m_MaxCount) {
            ReleaseOverflowObj();
        }

        m_ObjPool.Push(obj);
        m_AfterRecycle?.Invoke(obj);
    }
}

可以看到, 我们增加了几个委托, 并在对应的位置调用:

  • UnityAction<T> m_BeforeSpawn: 产生对象之前通知
  • UnityAction<T> m_AfterRecycle: 回收对象之前通知
  • UnityAction<T> m_AfterRelease: 销毁对象之前通知
  • NewObj m_NewObjDelegate: 生成对象

最后实现接口IDisposable, 在对象池销毁时清理这些委托, 养成良好的清理委托的习惯, 避免在如Xlua中使用时造成无法正常垃圾回收.

顺便说一句: m_AfterRecycle?.Invoke(obj);的等价于if (m_AfterRecycle != null) m_AfterRecycle(obj);

使用示例

使用很简单, 也就是需要的时候申请, 不需要的时候回收而已.

public class Animal {
    public int id;
    public string name;

    public void Reset() {
        id = 0;
        name = string.Empty;
    }
}

public class ObjectPoolTest {
    public static void Test() {
        var objectPool = new ObjectPool<Animal>(1, 10, 5,
            animal => {
                Debug.Log(string.Format("before spawn code: {0} ", animal.GetHashCode()));
            }, 
            animal => {
                Debug.Log(string.Format("after recycle code: {0}, name: {1}", animal.GetHashCode(), animal.name));

                animal.Reset();
            },
            animal => {
                Debug.Log(string.Format("after release code: {0}", animal.GetHashCode()));
            });

        var index = 1;
        var lst = new List<Animal>();
        for(var i = 0; i < 20; i++) {
            var animal = objectPool.Spawn();
            animal.id = i;
            animal.name = "name: " + index++;

            lst.Add(animal);
        }

        for(var i = 18; i > 0; i--) {
            var animal = lst[i];
            objectPool.Recycle(animal);

            lst.Remove(animal);
        }

        for(var i = 0; i < 2; i++) {
            var animal = objectPool.Spawn();
            animal.id = i;
            animal.name = "name: " + index++;

            lst.Add(animal);
        }

        foreach(var animal in lst) {
            objectPool.Recycle(animal);
        }

        objectPool.Dispose();
    }
}

我们产生了一些对象, 然后使用, 然后回收, 然后又产生了一些对象, 最后回收所有对象.

整个过程会达到最大数量的对象, 然后一直回收利用. 这里就不贴输出了, 有兴趣的同学可以自行尝试.

最后

今天分享了普通类的对象池, 下一篇文章我们将使用对象池的技术来管理Unity中的GameObject对象.

好了, 今天就这样, 希望对大家有所帮助.