13-25.【Combine】Combine 如何处理异步任务的取消?(例如网络请求或高频输入)

0 阅读2分钟

在 Combine 中,异步任务的取消机制是基于声明式生命周期管理的。其核心在于 AnyCancellable 对象和订阅链的自动清理机制。

以下是处理取消操作的底层逻辑和实战技巧:

1. 核心机制:Cancellable 协议

在 Combine 中,所有的订阅操作(如 .sink.assign)都会返回一个遵循 Cancellable 协议的对象,通常是 AnyCancellable

  • 自动取消:当 AnyCancellable 对象被销毁(Deinit)时,它会自动调用内部的 cancel() 方法,从而掐断整个订阅链。
  • 资源清理:一旦 cancel() 被调用,下游不再接收数据,上游(如 URLSession)会立即停止副作用(中断网络连接)。

2. 实战场景:如何有效管理取消

A. 批量生命周期绑定(最常用)

ViewModelController 中,使用一个集合存储所有的订阅。当对象销毁时,所有任务一并取消。

Swift

class SearchViewModel: ObservableObject {
    private var cancellables = Set<AnyCancellable>() // 存储池

    func startTask() {
        URLSession.shared.dataTaskPublisher(for: url)
            .sink(receiveCompletion: { _ in }, receiveValue: { _ in })
            .store(in: &cancellables) // 绑定生命周期
    }
}

B. 高频输入的“覆盖式”取消

对于搜索框等高频输入,我们不希望旧请求堆积。使用 .switchToLatest() 是最高效的方案。

  • 原理:当新的 Publisher 到达时,它会自动发送取消信号给前一个尚未完成的 Publisher。

Swift

$searchText
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .map { text in 
        return API.fetch(text) // 返回一个新的网络流
    }
    .switchToLatest() // 关键:自动取消之前的请求
    .sink { results in self.results = results }
    .store(in: &cancellables)

C. 手动精准取消

如果你需要中途根据特定业务逻辑停止某个任务,可以保留该订阅的引用并手动触发:

Swift

let downloadTask = downloader.publish()
    .sink { _ in }

// 比如用户点击了“取消下载”按钮
downloadTask.cancel() 

3. 底层是如何“掐断”网络请求的?

当你调用 cancel() 时:

  1. 信号通过订阅链反向传播到上游。
  2. URLSession.DataTaskPublisher 的内部实现会调用底层 URLSessionTasktask.cancel()
  3. 系统内核收到指令,关闭对应的 Socket 连接,释放内存和带宽。

4. 特殊注意事项

  • 闭包捕获循环:在 sink 中如果强引用了 self,且 self 持有了 cancellables,会导致内存泄漏,订阅永远无法自动取消。务必使用 [weak self]

  • Side Effects(副作用) :使用 handleEvents 操作符可以监控取消发生的时机:

    Swift

    .handleEvents(receiveCancel: { print("任务被取消了") })
    
  • SwiftUI 自动管理:在 SwiftUI 中,使用 .onReceive(_:perform:) 或新的 .task 钩子,SwiftUI 会根据视图的显示和消失状态自动处理订阅的开始和取消。

总结

  • 普通任务:扔进 Set<AnyCancellable> 随对象生命周期自动管理。
  • 高频覆盖:用 switchToLatest 实现“喜新厌旧”的自动取消。
  • 手动控制:保存特定的 AnyCancellable 变量并调用 .cancel()