13-24.【Combine】Combine 与 Swift Concurrency (async/await) 可以如何共存?

52 阅读2分钟

在 Swift 现代开发中,CombineSwift Concurrency (async/await) 并非互斥关系,而是互补关系。

Combine 擅长处理连续的、高频的、可操作的事件流(如 UI 交互、传感器数据);而 async/await 擅长处理一次性的、线性的异步任务(如单个网络请求、文件 IO)。

以下是它们共存并协作的四个核心维度:


1. 将 Combine 流转换为 AsyncSequence

如果你有一个现成的 Combine Publisher,但想在异步环境(如 Task)中使用 for-in 循环来消费它,可以使用 .values 属性。

  • 原理.values 将 Publisher 包装成一个遵循 AsyncSequence 协议的对象。
  • 场景:在 Task 中顺序处理通知、监听状态变化。

Swift

let publisher = NotificationCenter.default.publisher(for: .userDidLogin)

Task {
    // 使用 Swift Concurrency 语法消费 Combine 流
    for await notification in publisher.values {
        print("用户已登录: (notification)")
    }
}

2. 在 Combine 链中调用异步函数

这是最常见的共存方式。当你需要在 Combine 的操作符(如 map)里执行一个 async 函数时,可以利用 flatMap 配合 Future 或直接在闭包中使用异步逻辑。

  • 最佳实践:使用 mapflatMap 来触发 Task

Swift

searchSubject
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .flatMap { query in
        // 将 async 函数包装进 Future
        Future { promise in
            Task {
                let result = await performAsyncSearch(query)
                promise(.success(result))
            }
        }
    }
    .assign(to: &$results)

3. 将异步函数封装为 Publisher

如果你正在从旧的 Combine 架构向 async/await 迁移,或者你希望利用 Combine 的强大操作符(如 retry, zip)来处理一个异步任务,你可以将 async 函数封装为 Future

  • 核心逻辑Future 是连接这两个世界的桥梁,它能捕获异步任务的单次执行结果。

Swift

func fetchDataPublisher() -> AnyPublisher<Data, Error> {
    Future { promise in
        Task {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                promise(.success(data))
            } catch {
                promise(.failure(error))
            }
        }
    }
    .eraseToAnyPublisher()
}

4. 架构分工:谁负责什么?

在混合架构中,建议遵循以下分层原则,以避免逻辑混乱:

维度推荐使用 Combine推荐使用 Async/Await
数据特性多值流 (持续不断的更新)单值/元组 (请求 -> 响应)
逻辑复杂度需要高阶转换 (Zip, CombineLatest)简单的线性逻辑、循环、条件分支
生命周期需要精确的 cancel() 手控依赖 Task 作用域自动取消
UI 绑定@Published 与 SwiftUI 的深度绑定配合 @MainActor 处理简单更新

5. 防御式避坑指南

  • 背压与性能AsyncSequence 的消费速度受限于循环体。如果 Combine 产生数据的速度极快(如高频传感器),而你的 for await 处理太慢,可能会导致内存中数据的积压。此时应优先在 Combine 层使用 bufferthrottle
  • 线程安全:Combine 依赖 Scheduler(如 DispatchQueue),而 Swift Concurrency 依赖 Executor。在两者切换时,务必明确数据的执行环境。建议在更新 UI 前显式使用 .receive(on: DispatchQueue.main) 或将 ViewModel 标注为 @MainActor

总结

  • Combine 负责“编排”:处理复杂的流控、聚合和频率限制。
  • Async/Await 负责“执行”:处理具体的、具体的、易读的异步逻辑块。