13-16.【Combine】switchToLatest 如何保证取消旧任务?

1 阅读3分钟

switchToLatest 保证取消旧任务的核心机制在于其对**订阅生命周期(Subscription Lifecycle)**的精细控制。

它是一个“高阶”操作符,专门处理“Publisher 的 Publisher”(即上游发送的值本身也是一个异步流)。它的工作原理可以拆解为以下三个层面:


1. 内部订阅者的动态接管

当你调用 switchToLatest 时,它内部维护了一个**单一活跃订阅(Active Subscription)**的引用。

  • 新流到达:每当上游发出一个新的内部 Publisher 时,switchToLatest 做的第一件事就是对当前正在运行的那个旧 Publisher 调用 cancel()
  • 原子切换:这个“取消旧的”与“订阅新的”动作是连续发生的。由于 Combine 的底层 Cancellable 协议规定 cancel() 必须立即停止数据生产,因此旧任务的资源(如正在进行的网络请求)会被立刻释放。

2. 结合 map 产生的“开关效应”

通常我们不会单独使用它,而是配合 map

Swift

inputSearchText
    .map { text in 
        return API.fetchResults(for: text) // 这里返回的是一个新的 Publisher
    }
    .switchToLatest() // 关键点:它只监听最后一次 map 产生的 Publisher
    .sink { ... }

执行逻辑:

  1. 用户输入 "A",map 创建了 Request_AswitchToLatest 订阅它。
  2. 用户输入 "AB",map 创建了 Request_B
  3. switchToLatest 察觉到新值,立即向 Request_A 发送 Cancel 信号。
  4. Request_A 对应的 URLSessionTask 收到取消信号,断开连接,节省流量。
  5. switchToLatest 开始订阅 Request_B

3. 资源清理的底层保证

为什么旧任务一定能被“杀掉”?

  • 内存引用链断裂:在 Combine 中,Publisher 的运行依赖于下游的 Demand(背压请求)。当 switchToLatest 取消订阅旧 Publisher 时,它会释放对旧订阅对象的强引用。
  • 副作用中止:如果是标准的 URLSession.DataTaskPublisher,其内部实现在收到 cancel 时会直接调用底层底层网络库的 task.cancel()。这确保了不仅下游收不到数据,OS 层级的系统资源也会被回收。

4. 典型场景:防止“数据竞争”

如果没有 switchToLatest(比如错误地使用了 flatMap),可能会出现以下诡异 Bug:

  1. 用户输入 "A",请求 A 发出。
  2. 用户输入 "AB",请求 B 发出。
  3. 请求 B 因为网络顺畅先回来了,UI 显示了 AB 的结果。
  4. 请求 A 因为延迟较晚回来,UI 被覆盖,显示了 A 的结果。

结果:搜索框显示 "AB",结果列表显示的却是 "A" 的内容。这就是经典的竞态条件(Race Condition)switchToLatest 通过“只认新欢”完美规避了此问题。


5. 注意事项:错误处理的“连带责任”

由于 switchToLatest 只有一个输出通道:

  • 致命伤:如果任何一个内部子流抛出了错误(Failure),且你没有在子流内部处理(使用 .catch),这个错误会穿透到 switchToLatest 的下游,导致整个主订阅链条永久关闭
  • 防御手段:在 map 内部的 Publisher 末尾加上 .catch { _ in Empty() },确保子流的失败不会“炸掉”主管道。

总结

switchToLatest 的核心价值在于:它将“异步任务的并存”转化为了“线性顺序的覆盖” 。它通过主动销毁旧的 Subscription 对象,利用 ARC 触发底层的资源清理。