什么是对象池
Unity中调用UnityEngine.Object.Instantiate
函数进行对象复制是开销较大(相对)的做法,每帧如果只会创建几个对象那完全不用在意性能万一一个对象特别复杂可能还是要在意一下 。但如果在一帧中,创建了上百甚至上千个对象(比如说生成一大堆弹幕),这一帧肯定会卡顿。可以使用对象池来缓解这个问题
对象池会包含许多已经初始化完毕的对象,我们可以从池中获取已创建好的对象,或者将使用完毕的对象返还到池中。
一般开发中,像Socket连接,线程才会用到对象池,因为创建这样的对象开销非常大。中小型对象就别用了,CLR内存分配速度几乎可以忽略不计,小对象的GC问题也可以通过struct来缓解。所以这里就不写通用对象池了,因为用不到(大概)。
实践
简易对象池
一个最最最简易的GameObject对象池可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ObjectPool { private readonly Queue<GameObject> _queue; private readonly GameObject _prefab; public ObjectPool (GameObject prefab, int initCount = 0 ) { _queue = new Queue<GameObject>(initCount); _prefab = prefab; for (var i = 0 ; i < initCount; i++) { Recycle(Allocate()); } } public GameObject Get () { var result = _queue.Count == 0 ? Allocate() : _queue.Dequeue(); result.SetActive(true ); return result; } public void Recycle (GameObject instance ) { instance.SetActive(false ); _queue.Enqueue(instance); } private GameObject Allocate () { UnityEngine.Object.Instantiate(_prefab); } }
其实就是封装了下队列和实例化对象的方法。但这样写有很多问题,先不说内存泄漏,就这个回收时没有任何检查,谁知道我返还回来的对象,是不是从这个池中实例化出来的呢?(能保证返还回来的一定是正确的话当我没说)
优化
为了解决脑抽返还了不是这个对象池实例化出来的对象,第一时间想到可以使用HashSet来记录对象,每次回收时检查一下。尽管HashSet的查询时间已经是O(1)级别了,但比起直接访问数组索引还是慢了那么点,有没有只使用数组办法呢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class ObjectPool { private readonly GameObject _template; private readonly List<GameObject> _pool; private readonly Stack<int > _usable; public IReadOnlyList<GameObject> Container => _pool; public ObjectPool (GameObject template, int initCount ) { _template = template; _pool = new List<GameObject>(initCount); _usable = new Stack<int >(initCount); for (var i = 0 ; i < initCount; i++) { var go = Instantiate(); var ptr = _pool.Count; _pool.Add(go); Return(ptr); } } private GameObject Instantiate () { return UnityEngine.Object.Instantiate(_template); } public int Get () { int ptr; GameObject go; if (_usable.Count <= 0 ) { go = Instantiate(); ptr = _pool.Count; _pool.Add(go); } else { ptr = _usable.Pop(); go = _pool[ptr]; } go.SetActive(true ); return ptr; } public void Return (int ptr ) { if (ptr >= _pool.Count) throw new ArgumentException(); _usable.Push(ptr); var go = _pool[ptr]; go.SetActive(false ); } }
由于对象创建完毕后放入数组,几乎不会再有变动,因此我们可以用数组储存对象,用一个栈来储存未被使用过的对象在数组中的下标。获取对象时从栈中弹出一个下标,需要访问对应的对象也可以通过Container属性加下标的形式。
简单,粗暴,有效(个人没出过问题)。当然这个只适合知道最多会创建多少个对象的时候,因为没有阈值可能会导致内存爆满的问题。
TODO:对象池爆满的问题