制作大量释放游戏特效的画面时,总会遇到帧率突然卡顿的时刻。
如果不断用init创建子弹、粒子这类生命周期短且很快消失的对象,就会在 Instruments 中看到内存图像像锯齿一样跳动。
这时就该请出Swift 对象池模式了。
先说结论:对象池是一种“不是每次创建新对象,而是提前创建后借用并归还”的复用技术,其目的与容易混淆的 Flyweight 完全不同。
本文将依次介绍对象池是什么、它与 Flyweight 有何不同,以及如何用 Swift 实际编写对象池。
什么是对象池模式?
对象池会预先创建多个创建成本较高的对象,并将它们放入“池”中。
需要时不再新建对象,而是从池中取出一个使用,用完后再归还。
核心是复用。目的是降低反复进行init和deinit所产生的内存分配与释放成本。
对象池的一句话总结是:“不要创建,借来使用再归还。”
它主要适用于以下场景。
- 像子弹、粒子一样在短时间内大量创建和销毁的对象
- 像网络连接、线程一样创建本身成本很高的资源
- 像
UITableViewCell的复用队列一样,在画面上不断替换的视图
其实 iOS 开发者早就使用过对象池。dequeueReusableCell就是 Apple 创建的对象池。
它与 Flyweight 有什么不同?
两者都给人“节省对象使用”的印象,因此很容易混淆。
但它们的目的完全不同。
对象池是让子弹这类对象错开时间轮流复用的方式。使用并归还当前子弹后,下一颗子弹就会接替它的位置。
Flyweight 是让多个位置同时引用同一个共享对象的方式。例如绘制森林中的 1 万棵树时,颜色、纹理等公共数据只保留一份,位置值单独传入。
整理成表格后,可以这样区分。
| 分类 | 对象池 | Flyweight |
|---|---|---|
| 目的 | 降低创建与释放成本 | 降低内存使用量 |
| 复用方式 | 借用并归还(时间分割) | 同时共享(空间分割) |
| 状态 | 每个对象保留独有状态 | 分离共享状态与外部状态 |
| 典型示例 | 单元格复用、粒子 | 字体字形、地图图标 |
一句话概括:对象池是“轮流复用”,Flyweight 是“大家共享”。
如何用 Swift 创建对象池
结构比想象中简单:一个存放可用对象的数组,再加上借出和归还的两个方法即可。
下面是一个用泛型编写的简单对象池。
final class ObjectPool<T> {
private var available: [T] = []
private let factory: () -> T
init(factory: @escaping () -> T) { self.factory = factory }
func acquire() -> T { available.popLast() ?? factory() } // 没有就新建
func release(_ item: T) { available.append(item) } // 用完后归还
}
acquire()会在池中有剩余对象时取出,只有池为空时才创建新对象。
通过release()归还后,下一个请求就会复用该对象。
实际应用到粒子后,流程如下。
let pool = ObjectPool<Particle> { Particle() }
let p = pool.acquire() // 从池中借用
p.reset(at: point) // 初始化状态很重要!
// ...在画面上用完后
pool.release(p) // 归还
这里有一点必须记住:归还的对象仍然保留之前的状态。
因此再次借出时,必须经过类似reset()的初始化,才能避免幽灵数据。
什么时候该用、什么时候该避免对象池?
因为好用就到处使用,反而可能适得其反。
这是我实际使用后整理出的判断标准。
推荐在以下情况使用。
- 每秒反复创建和销毁几十个以上对象时
- 创建单个对象的成本明显较高时
- 内存图像像锯齿一样跳动,并且能看到 GC(垃圾回收)/ARC(Automatic Reference Counting,自动引用计数)的负担时
遇到以下情况请重新考虑。
- 如果只是偶尔创建一两个轻量对象,管理对象池的成本反而更高
- 如果忘记归还,池会被耗空,反而每次都要新建对象
- 在多线程环境中,访问对象池需要锁或队列同步
Swift 拥有很多值类型(struct),ARC 也相当高效,因此不必默认优先引入对象池。
建议先用 Instruments 测量,确认瓶颈后再引入。
对象池是节省创建成本的复用模式,而 Flyweight 是节省内存的共享模式。
只要牢牢记住两者的区别,就能根据场景选择合适的方法。
先测量,再在需要时精准引入。希望你也能亲身体验帧率变得更加流畅的时刻。

