ゲームエフェクトを大量に表示する画面を作っていると、フレームが急にカクつくことがあります。
弾丸やパーティクルのように短命なオブジェクトを毎回init生成していると、Instrumentsでメモリグラフがノコギリ状に跳ねるのがわかります。
そんなときに登場するのがSwiftオブジェクトプールパターンです。
先に結論を言うと、オブジェクトプールは「オブジェクトを毎回新しく作らず、あらかじめ作ったものを借りて返す」再利用手法です。混同されやすいFlyweightとは、そもそも目的が異なります。
この記事では、オブジェクトプールとは何か、Flyweightとどう違うのか、そしてSwiftでの実装方法まで順に解説します。
オブジェクトプールパターンとは?
オブジェクトプールは、生成コストの高いオブジェクトをあらかじめ複数作り、「プール」に入れておく方式です。
必要なときに新しく作るのではなく、プールから取り出して使い、使い終わったら返却します。
核心は再利用です。initとdeinitを繰り返すことで発生するメモリの確保・解放コストを減らします。
オブジェクトプールを一言でまとめると、「作らずに借りて返す」です。
主に次のような場面で効果を発揮します。
- 弾丸やパーティクルのように短時間で大量に生成・破棄されるオブジェクト
- ネットワーク接続やスレッドのように、生成そのものが重いリソース
UITableViewCellの再利用キューのように、画面上で何度も入れ替えるビュー
実はiOS開発者なら、すでにオブジェクトプールを使っています。dequeueReusableCellはAppleが用意したオブジェクトプールだからです。
Flyweightとは何が違う?
どちらも「オブジェクトを節約して使う」印象があるため、混同しやすいものです。
しかし、目的はまったく異なります。
オブジェクトプールは、弾丸のようなオブジェクトを時間をずらして使い回す方式です。今の弾丸を使って返却すると、次の弾丸がその場所を引き継ぎます。
Flyweightは、複数の場所から同時に1つの共有オブジェクトを参照する方式です。たとえば森に1万本の木を描く場合、色やテクスチャなどの共通データは1つだけ持ち、位置だけを別に渡します。
表にまとめると、次のように分かれます。
| 区分 | オブジェクトプール | Flyweight |
|---|---|---|
| 目的 | 生成・破棄コストの削減 | メモリ使用量の削減 |
| 再利用方式 | 借りて返す(時間分割) | 同時に共有する(空間分割) |
| 状態 | オブジェクトごとに固有の状態を保持 | 共有状態と外部状態を分離 |
| 代表例 | セルの再利用、パーティクル | フォントグリフ、地図アイコン |
一言でいえば、オブジェクトプールは「順番に使い回す」、Flyweightは「みんなで共有する」です。
Swiftでオブジェクトプールを作る方法
構造は思ったよりシンプルです。利用可能なオブジェクトを格納する配列1つと、貸し出し・返却用のメソッド2つがあれば十分です。
以下はジェネリックで実装したシンプルなプールです。
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()のような初期化を必ず行い、ゴーストデータを防ぐ必要があります。
オブジェクトプールを使うべきとき、避けるべきとき
便利だからとどこでも使うと、かえって逆効果になります。
実際に使って整理した基準を紹介します。
次のような場合におすすめです。
- 1秒あたり数十個以上の生成・破棄が繰り返されるとき
- オブジェクト1個の生成コストが明らかに大きいとき
- メモリグラフがノコギリ状に跳ね、GC(ガベージコレクション)/ARC(Automatic Reference Counting、自動参照カウント)の負荷が見えるとき
次のような場合は考え直してください。
- たまに1〜2個作るだけの軽量なオブジェクトなら、プール管理のコストのほうが大きくなります
- 返却を忘れるとプールが空になり、かえって毎回新しく作ることになります
- マルチスレッド環境では、プールへのアクセスにロックやキューの同期が必要です
Swiftは値型(struct)が多く、ARCもかなり効率的なので、必ずしも最初からプールを導入する必要はありません。
まずInstrumentsで測定し、ボトルネックを確認してから導入するのがおすすめです。
オブジェクトプールは生成コストを抑える再利用パターンで、Flyweightはメモリを抑える共有パターンです。
違いをしっかり押さえておけば、状況に合った手法を選べます。
まず測定し、必要なときにぴったり合う形で導入してみてください。フレームが一段となめらかになる瞬間を、ぜひ体験してください。

