13-28.【Combine】如何分析 Combine 流的 retain cycle / memory leak?典型原因有哪些?

2 阅读2分钟

分析 Combine 的内存泄漏(Memory Leak)或循环引用(Retain Cycle)其实和分析普通的 Swift 闭包类似,但因为 Combine 是声明式且具有链式调用的特性,引用关系往往隐藏在操作符(Operators)之中。

以下是分析工具、典型原因以及解决方案的完整指南。


1. 如何分析与定位?

在 Swift 中分析 Combine 的内存问题,通常遵循“由易到难”的三个步骤:

A. 使用 Xcode Memory Graph Debugger

这是最直观的方法。运行 App 后,点击 Xcode 底部调试栏的 "Memory Graph" 图标(三个小圆圈连接的图标)。

  • 观察: 搜索你的 Cancellable 存储对象(如 AnyCancellable)或相关的 ViewModel
  • 线索: 如果你看到一个本该被销毁的对象依然存在,且连线图中出现一个循环箭头,那就是发生了循环引用。

B. 利用 deinit 日志

最简单有效的“土办法”。在你怀疑泄漏的对象(如 ViewControllerViewModel)中添加:

Swift

deinit {
    print("(Self.self) 销毁了")
}

如果页面关闭了但日志没出,说明必然存在持有的引用。

C. 使用 handleEvents 操作符

Combine 提供了 handleEvents 来监控流的生命周期:

Swift

publisher
    .handleEvents(receiveCancel: { print("流被取消了") })
    .sink { ... }
    .store(in: &cancellables)

2. 典型的泄漏原因

原因一:在 sink 中强引用 self(最常见)

当你使用 .sink 并在闭包内调用 self 的方法或属性时,sink 返回的 Cancellable 被存储在 self.cancellables 中,形成了闭环。

  • 错误示例:

    Swift

    self.viewModel.$data
        .sink { value in
            self.updateUI(value) // 这里的 self 被强引用了
        }
        .store(in: &cancellables) // self 持有 cancellables,cancellables 持有 sink,sink 持有 self
    

原因二:在 assign(to:on:) 中强引用

.assign(to: .property, on: self) 会强引用 on 传入的对象,直到流结束。

  • 风险点: 如果流是一个无限流(比如 TimerPassthroughSubject),self 永远不会被释放。
  • 注意: 在 iOS 14+ 中,使用 assign(to: &$publishedProperty) 语法可以安全地避免这个问题,因为它不创建强引用。

原因三:自定义操作符或闭包内部

mapflatMapfilter 这样的操作符内部如果捕获了 self,也会导致同样的问题。


3. 解决方案

方案 A:使用 [weak self]

这是解决闭包循环引用的标准做法。

Swift

.sink { [weak self] value in
    guard let self = self else { return }
    self.updateUI(value)
}

方案 B:使用 assign(to: &$...)

如果你是更新 @Published 属性,请优先使用这种方式:

Swift

// 这种方式会自动处理生命周期,不会产生循环引用
viewModel.$status
    .assign(to: &$status) 

方案 C:手动管理生命周期

如果某些流需要提前结束,可以在适当的时机手动调用 cancellable.cancel() 或将存储容器置空:

Swift

self.cancellables.removeAll()

总结对照表

场景潜在风险推荐对策
.sink强捕获 self 导致闭环使用 [weak self]
.assign(to:on:)on 对象强引用使用 assign(to: &$) (iOS 14+)
无限流 (Timer/Subject)Cancellable 永远不释放结合生命周期手动调用 .cancel()
复杂的 flatMap内部闭包捕获外部变量仔细检查嵌套闭包的捕获列表