軟體設計

Swift 物件池模式完整整理(與 Flyweight 的差異・再利用方法)

製作大量產生遊戲特效的畫面時,總會遇到畫面突然一頓一頓的時刻。

閱讀 4 分鐘
Swift 物件池模式完整整理(與 Flyweight 的差異・再利用方法) 封面圖

製作大量產生遊戲特效的畫面時,總會遇到畫面突然一頓一頓的時刻。

如果持續用init建立像子彈或粒子這種生命週期很短、很快消失的物件,就會在 Instruments 中看到記憶體圖表像鋸齒一樣跳動。

這時就輪到Swift 物件池模式登場了。

先說結論:物件池是一種「不每次建立新物件,而是預先建立後借用並歸還」的再利用技術,目的本身就和容易混淆的 Flyweight 不同。

本文會依序說明物件池是什麼、和 Flyweight 有何不同,以及如何用 Swift 實際實作。


什麼是物件池模式?

物件池會預先建立多個建立成本高的物件,並將它們放進「池」中。

需要時不是重新建立,而是從池中取出使用,使用完再歸還。

核心就是再利用。目的是降低反覆進行initdeinit所產生的記憶體配置與釋放成本。

物件池的一句話總結是:「不要建立,借來使用再歸還。」

它主要適合以下情境。

  • 像子彈、粒子一樣在短時間內大量建立與銷毀的物件
  • 像網路連線或執行緒一樣,建立本身成本很高的資源
  • UITableViewCell的再利用佇列一樣,在畫面上持續替換的 View

其實 iOS 開發者早就使用過物件池了。dequeueReusableCell就是 Apple 建立的物件池。


和 Flyweight 有什麼不同?

兩者都給人「節省使用物件」的印象,因此很容易混淆。

但它們的目的完全不同。

物件池是讓像子彈這類物件錯開時間輪流使用的方式。現在這顆子彈使用完並歸還後,下一顆子彈就會接替它的位置。

Flyweight 是讓多個地方同時參考同一個共用物件的方式。例如繪製森林中的 1 萬棵樹時,顏色、材質等共用資料只保留一份,位置值則另外傳入。

整理成表格後,可以分成以下幾點。

分類 物件池 Flyweight
目的 降低建立與釋放成本 降低記憶體使用量
再利用方式 借用並歸還(時間分割) 同時共用(空間分割)
狀態 每個物件維持獨有狀態 分離共用狀態與外部狀態
代表範例 Cell 再利用、粒子 字型字形、地圖圖示

一句話總結:物件池是「輪流再利用」,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()這樣的初始化,才能避免幽靈資料。


什麼時候該用、什麼時候該避免物件池?

覺得好用就到處使用,反而可能造成反效果。

這是我實際使用後整理出的判斷標準。

建議在這些情況使用。

  1. 每秒反覆建立與銷毀數十個以上物件時
  2. 建立單一物件的成本明顯很高時
  3. 記憶體圖表像鋸齒一樣跳動,並且看得出 GC(垃圾回收)/ARC(Automatic Reference Counting,自動參照計數)的負擔時

這些情況請先重新考慮。

  • 如果只是偶爾建立一兩個輕量物件,管理物件池的成本反而更高
  • 如果忘了歸還,池子會被用空,反而每次都必須重新建立
  • 在多執行緒環境中,存取物件池需要鎖定或佇列同步

Swift 有很多值型別(struct),ARC 也相當有效率,因此不必無條件優先導入物件池。

建議先用 Instruments 測量,確認瓶頸後再導入。

實際測量後,首先可以確定記憶體圖表平穩了許多
實際測量後,首先可以確定記憶體圖表平穩了許多

物件池是節省建立成本的再利用模式,而 Flyweight 是節省記憶體的共用模式。

只要確實掌握兩者的差異,就能依情境選擇合適的方法。

先測量,再在需要時精準導入。希望你也能親身體驗畫面幀率變得更加流暢的時刻。

延伸閱讀