13-5.【Combine】Combine 的 Cancellable 是如何工作的?

0 阅读2分钟

在 Combine 中,Cancellable(及其最常见的实现 AnyCancellable)扮演的是**“资源租约”“生命周期控制器”**的角色。

理解它的工作原理可以从以下四个层面拆解:


1. 本质:一种“清理”协议

Cancellable 本身是一个非常简单的协议,只定义了一个方法:

Swift

protocol Cancellable {
    func cancel()
}

它的核心职责是手动切断订阅者(Subscriber)与发布者(Publisher)之间的连接。一旦调用 cancel(),发布者将停止发送数据,且该订阅链条占用的所有内存资源都会被释放。


2. 工作原理:RAII 模式

AnyCancellable 是该协议的类型擦除实现,它利用了 Swift 的 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 机制:

  • 初始化(订阅开始) :当你调用 .sink.assign 时,系统会创建一个 Subscription 对象,并将其包装在 AnyCancellable 中返回给你。
  • 持有(生命周期绑定) :只要你持有着这个 AnyCancellable 变量(通常存在 Set<AnyCancellable> 中),订阅就有效。
  • 销毁(自动取消)这是最重要的特性。当 AnyCancellable 变量被销毁(例如它所属的 ViewModel 被释放,或者包含它的 Set 被清空)时,它的 deinit 方法会自动被调用。在 deinit 内部,它会自动执行 cancel()

3. 为什么需要 store(in: &cancellables)

很多初学者会疑惑:为什么不能直接写 .sink { ... } 而非要存起来?

  • 如果不存储.sink 返回的 AnyCancellable 是一个局部变量。函数执行结束,该变量立即被销毁,触发 deinit,导致订阅在还没来得及收到任何数据时就瞬间关闭了。
  • 存储在 Set 中:通过将它存入 Set,你将订阅的生命周期交给了这个 Set。只要这个 Set(通常是类的一个属性)还活着,订阅就一直活着。

4. 内部链路:Cancel 是如何传递的?

当你调用 cancel() 时,信号会逆流而上:

  1. 用户销毁 AnyCancellable 或手动调用 cancel()
  2. AnyCancellable 调用内部 Subscriptioncancel()
  3. Subscription 收到指令,停止向上游 Publisher 请求数据(Backpressure 停止)。
  4. Publisher 收到取消信号,释放对闭包、网络请求或定时器的引用。

5. 防御式编程技巧

  • 防止重复订阅: 如果你在方法里多次调用 .store(in:) 往同一个 Set 里存,会产生多个活跃订阅。如果你的意图是“每次只保留最新的订阅”,你应该将 AnyCancellable 存为一个可选属性:

    Swift

    var searchTask: AnyCancellable?
    
    func search(text: String) {
        // 赋值新任务时,旧任务会自动销毁并 cancel()
        searchTask = api.fetch(text).sink(...) 
    }
    
  • 手动清理: 如果你想提前停止所有任务,只需调用 cancellables.removeAll()


总结

Cancellable 是 Combine 内存安全的基础。它通过 Swift 的引用计数机制(ARC)确保了:当一个对象(如 View/ViewModel)消失时,它发起的异步任务(网络、定时器等)必须随之消失,从而彻底杜绝内存泄漏和野指针回调。