軟體設計

Swift Flyweight Pattern:透過共享物件節省記憶體

開發 App 時,有時需要建立數千甚至數萬個相似物件。最典型的例子是在地圖上撒出數萬個標記;在這種情況下,記憶體圖表很容易持續攀升。

閱讀 4 分鐘
Swift Flyweight Pattern:透過共享物件節省記憶體 封面圖

開發 App 時,有時需要建立數千甚至數萬個相似物件。最典型的例子是在地圖上撒出數萬個標記;在這種情況下,記憶體圖表很容易持續攀升。

這時就可以考慮 Flyweight Pattern。簡單說,就是把多個物件共用的資料合併後共享,大幅降低記憶體用量。不要每次都重新建立相同的東西,而是建立一次後共同使用。

本文將介紹 Swift 中的 Flyweight Pattern、適合使用的時機,以及如何用實際程式碼實作。

Flyweight Pattern 到底是什麼?

Flyweight 的意思是「輕量物件」,也是拳擊中最輕量級的名稱。

概念很簡單:把物件所擁有的資料分成兩種類型。

  • 內在狀態(intrinsic):可由多個物件共享、且不會變動的資料,例如樹木種類、紋理和色彩。
  • 外在狀態(extrinsic):每個物件各不相同、會隨情況變動的資料,例如樹木的位置或大小。

想像一下要繪製一座森林。如果要畫 100 萬棵樹,但它們其實只有松樹、橡樹和楓樹這三種,會怎麼做?

紋理和色彩資訊(內在狀態)只要建立三份並共享,位置值(外在狀態)則另外保存 100 萬份即可。

重量級的共用資料只建立一份並共享,個別的輕量資料再分開管理。這就是 Flyweight 的全部概念。

那天撒出標記後看到記憶體飆升,我花了好一陣子才處理好。
那天撒出標記後看到記憶體飆升,我花了好一陣子才處理好。

用 Swift 程式碼實作看看

只用文字不太容易理解,我們直接來看程式碼,把剛才的樹木範例原樣改寫過來。

首先是用來儲存要共享之內在狀態的 Flyweight 物件。

// 共享的重量級資料(內在狀態)
final class TreeType {
    let name: String
    let color: String
    let texture: Data  // 大型紋理

    init(name: String, color: String, texture: Data) {
        self.name = name
        self.color = color
        self.texture = texture
    }
}

接著建立管理並共享這些 TreeType 的工廠。如果某個種類已經建立,就不再新增,而是直接重複使用。

// 相同種類不重新建立,直接重複使用
final class TreeFactory {
    private var pool: [String: TreeType] = [:]

    func treeType(name: String, color: String, texture: Data) -> TreeType {
        let key = "\(name)-\(color)"
        if let existing = pool[key] { return existing }
        let type = TreeType(name: name, color: color, texture: texture)
        pool[key] = type
        return type
    }
}

現在實際的樹木只保存位置等外在狀態,重量級資料則只參照 TreeType。

由工廠持有資料,樹木只攜帶參照的結構。
由工廠持有資料,樹木只攜帶參照的結構。
// 個別樹木只保存位置,種類透過參照共享
struct Tree {
    let x: Double
    let y: Double
    let type: TreeType  // 共享物件參照
}

即使種下 100 萬棵樹,TreeType 實例也只會有種類數量那麼多(例如 3 個)。其餘全部只是指向這 3 個實例的參照。

即使繪製整座森林,也只要共享 3 個紋理就夠了。
即使繪製整座森林,也只要共享 3 個紋理就夠了。

什麼時候該用,什麼時候不該用?

Flyweight 不一定永遠是正解。有時不使用反而更好。以下按情境整理。

情境 Flyweight
大量建立相同性質的物件 適合
物件擁有可共享的重量級資料 適合
物件數量少 不必要(反而只會增加複雜度)
大部分狀態都因物件而異 效果有限

當大量物件與可共享的重量級資料這兩個條件同時存在時,就是 Flyweight 發揮作用的場景。

在 Swift 中,class 作為參考型別本來就會透過參照共享,因此當值型別 struct 整個放入重量級資料而導致頻繁複製時,效果尤其明顯。


常見問題

Q. 和 Singleton Pattern 有什麼不同?

Singleton 會強制類別只能有一個實例。Flyweight 則允許存在多個實例,但只挑選可共享的部分來共享。例如樹木種類有 3 種,而樹木本身有 100 萬棵。

Q. 我常常把它和快取搞混。

目的不同。快取是因為「不想再次計算或查詢」而保存結果;Flyweight 則是為了「節省記憶體」而管理共享物件。不過,工廠的 pool 確實會像快取一樣運作。

Q. Swift Standard Library 也有使用嗎?

有。例如字串駐留和小整數快取等最佳化,都採用了類似的概念:不反覆建立相同的值,而是加以共享。


Flyweight Pattern 名字聽起來很難,但概念其實非常簡單:「重複的重量級資料只建立一份,大家一起用。」

如果需要大量處理相似物件,例如地圖標記、粒子、Tile Map 或文字算繪,不妨考慮一下。你會感受到記憶體圖表明顯變得平穩。

延伸閱讀