在 Swift 中,ARC(自动引用计数)与 Combine 的交互是一场关于生命周期绑定的博弈。如果不理解它们之间的引用关系,很容易陷入“订阅立即失效”或“内存永久泄漏”的两个极端。
核心逻辑可以概括为:订阅关系(Subscription)的存续,完全依赖于 ARC 对 Cancellable 对象的持有。
1. 引用链的“强拉力”:谁持有了谁?
当一个完整的 Combine 链条建立时,ARC 内部会形成一条从下往上的强引用链:
- 你持有
AnyCancellable:通常存放在Set<AnyCancellable>中。 AnyCancellable持有Subscription:这是连接发布与订阅的纽带。Subscription持有Subscriber:因为订阅者需要处理数据。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 中,减少大闭包的使用 |