13-4.【Combine】Swift 的 ARC 与 Combine 的 Publisher/Subscriber 链有什么交互?

5 阅读3分钟

在 Swift 中,ARC(自动引用计数)与 Combine 的交互是一场关于生命周期绑定的博弈。如果不理解它们之间的引用关系,很容易陷入“订阅立即失效”或“内存永久泄漏”的两个极端。

核心逻辑可以概括为:订阅关系(Subscription)的存续,完全依赖于 ARC 对 Cancellable 对象的持有。


1. 引用链的“强拉力”:谁持有了谁?

当一个完整的 Combine 链条建立时,ARC 内部会形成一条从下往上的强引用链:

  1. 你持有 AnyCancellable:通常存放在 Set<AnyCancellable> 中。
  2. AnyCancellable 持有 Subscription:这是连接发布与订阅的纽带。
  3. Subscription 持有 Subscriber:因为订阅者需要处理数据。
  4. Subscriber 间接持有 Publisher 的闭包/算子:为了执行变换逻辑。

防御式警示:如果你在函数内部创建了一个订阅链,但没有将其赋值给任何变量,ARC 会在函数执行结束的一瞬间销毁这个局部变量。结果就是:订阅动作刚刚开始,就被 ARC 强行切断(Cancel)了。


2. 内存泄漏的重灾区:sink 中的循环引用

这是最常见的 Bug 来源。当你在 sink 闭包中引用了 self(通常是 ViewModel),而 self 又持有存储 Cancellable 的集合时,一个闭环形成了。

Swift

class MyViewModel: ObservableObject {
    var cancellables = Set<AnyCancellable>()
    @Published var data = ""

    func startTask() {
        somePublisher
            .sink { value in
                // ❌ 危险:self 持有 cancellables,cancellables 持有这个闭包,闭包又持有 self
                self.data = value 
            }
            .store(in: &cancellables)
    }
}

解决方案:

  • 使用 [weak self] :这是标准做法。
  • 使用 assign(to: &...) :在 Swift 5.3+ 中,这种语法不会产生循环引用,因为它会自动处理生命周期。

3. assign(to:on:) 的特殊隐患

早期的 assign(to: .data, on: self) 有一个非常隐蔽的坑:它会强引用 on 对应的对象

  • 如果 self 持有这个订阅,而这个订阅又强引用了 self,那么这个 ViewModel 将永远无法被释放。
  • 防御建议:在处理当前对象的属性绑定时,永远优先使用 assign(to: &$data)

4. 操作符对生命周期的干预

某些操作符会改变 ARC 的表现:

  • .share().multicast() :它们会在多个订阅者之间共享上游。只要还有一个订阅者存活,上游 Publisher 就不会被 ARC 释放。
  • .flatmap:它会产生新的内部 Publisher。如果处理不当(如内部 Publisher 永不结束且被强引用),会导致内存占用持续攀升。

5. 调试工具:handleEvents

如果你怀疑某个订阅链由于 ARC 原因被提前意外终止,可以使用 handleEvents 来追踪:

Swift

somePublisher
    .handleEvents(receiveCancel: {
        print("🚨 订阅被取消了!请检查 Cancellable 是否被销毁。")
    })
    .sink { ... }

总结:ARC 与 Combine 的黄金守则

情况后果防御手段
未存储 Cancellable订阅立即结束,不触发逻辑使用 .store(in: &cancellables)
sink 中直接用 self内存泄漏(Retain Cycle)使用 [weak self]assign(to:)
长链条闭包嵌套复杂的内存拓扑图,难以排查尽量将逻辑写在自定义 Operator 中,减少大闭包的使用