软件设计

Swift Flyweight Pattern:通过共享对象节省内存

开发应用时,有时需要创建数千甚至数万个相似对象。典型例子是在地图上放置数万个标记;在这种情况下,内存图很容易持续攀升。

4 分钟阅读
Swift Flyweight Pattern:通过共享对象节省内存 封面图

开发应用时,有时需要创建数千甚至数万个相似对象。典型例子是在地图上放置数万个标记;在这种情况下,内存图很容易持续攀升。

这时可以考虑 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 或文本渲染,不妨考虑一下。你会发现内存图明显变得平稳。

延伸阅读