Swift 并发与 Combine 详解
Swift 提供了两种强大的异步编程范式:Swift 并发模型 (async/await、Task、Actor)和Combine 响应式框架。它们各有优势,可单独使用也可协同工作,解决不同类型的异步编程问题。
一、Swift 并发模型(Swift 5.5+)
核心概念
Swift 并发模型基于结构化并发 和actor 模型,提供了更直观、更安全的异步编程方式,替代了传统的回调地狱。
| 组件 | 作用 | 关键特性 |
|---|---|---|
async/await |
异步函数标记与等待 | 让异步代码看起来像同步代码,消除嵌套回调 |
Task |
异步任务单元 | 可取消、有优先级、支持任务组 |
TaskGroup |
管理一组相关任务 | 等待所有任务完成、聚合结果、支持取消传播 |
Actor |
数据隔离与同步 | 确保状态安全访问,自动处理线程同步 |
Sendable |
值类型 / 函数安全传递 | 确保跨线程传递的数据是线程安全的 |
MainActor |
主线程执行保证 | 标记 UI 相关代码在主线程执行 |
基础用法示例
Swift
// 异步函数定义
func fetchUserData(userId: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// 调用异步函数
Task {
do {
let user = try await fetchUserData(userId: 123)
print("用户数据: \(user)")
} catch {
print("获取用户数据失败: \(error)")
}
}
// 任务组并行执行
func fetchMultipleUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUserData(userId: id)
}
}
var users = [User]()
for try await user in group {
users.append(user)
}
return users
}
}
核心优势
- 可读性强:异步代码线性编写,无回调嵌套
- 错误处理自然 :使用
try/catch处理异步错误,与同步代码一致 - 结构化并发:任务层级清晰,自动管理生命周期,防止资源泄漏
- 线程安全 :通过
Actor和Sendable确保数据访问安全
第一部分:Combine 超详细精讲
核心定位
Combine 是苹果官方的响应式编程框架 ,专门处理随时间变化的异步事件 (网络请求、按钮点击、输入框输入、定时器、KVO)。一句话总结:生产者发数据 → 管道加工数据 → 消费者用数据,全程链式调用,无回调嵌套。
1. Combine 四大核心组件(死记硬背)
| 组件名 | 作用 | 必知细节 |
|---|---|---|
| Publisher (发布者) | 生产 / 发送数据 | 1. 不订阅就不发数据;2. 只能发 3 种事件:值、完成、错误;3. 泛型:<输出值类型,错误类型> |
| Subscriber (订阅者) | 接收 / 处理数据 | 最常用 2 种:sink(接收值 + 完成)、assign(绑定到属性) |
| Operator (操作符) | 加工数据 | 链式调用,转换 / 过滤 / 合并数据(map/filter/debounce) |
| Cancellable (订阅持有者) | 保留订阅 | 不持有就直接销毁订阅 ,必须存到 Set<AnyCancellable> |
2. 最基础的完整流程(逐行解析)
Swift
import Combine
// 1. 创建发布者:Just = 发一个值就结束,错误类型Never(永不报错)
let publisher = Just("Hello Combine")
// 2. 订阅持有者:必须创建,否则订阅瞬间销毁
var cancellables = Set<AnyCancellable>()
// 3. 订阅 + 处理数据
publisher
.sink(
receiveCompletion: { completion in
// 接收完成事件:只有2种情况 .finished(正常结束)/.failure(报错)
print("订阅结束: \(completion)")
},
receiveValue: { value in
// 接收数据
print("收到值: \(value)")
}
)
.store(in: &cancellables) // 4. 保存订阅(核心!不写这行=白写)
✅ 输出:收到值: Hello Combine订阅结束: finished
3. 核心发布者:Subject(手动发数据,最常用)
Subject 是可以手动调用 send () 发值 的发布者,分两种,细节区别必考:
① PassthroughSubject(不存历史值)
- 只发给当前已订阅的订阅者
- 订阅前发的值,收不到
Swift
let subject = PassthroughSubject<String, Never>()
subject.send("第一条") // 订阅前发的,收不到
// 订阅
subject.sink{ print($0) }.store(in: &cancellables)
subject.send("第二条") // 收到
subject.send(completion: .finished) // 结束
✅ 输出:第二条
② CurrentValueSubject(保存最新值)
- 订阅瞬间会收到最后一次发送的值
- 必须初始化一个默认值
Swift
let subject = CurrentValueSubject<String, Never>("默认值")
subject.sink{ print($0) }.store(in: &cancellables) // 订阅直接收默认值
subject.send("新值")
✅ 输出:默认值 → 新值
4. 必学操作符(工作中 90% 使用率)
① map:转换数据类型
Swift
Just(5)
.map { $0 * 2 } // 5 → 10
.sink{ print($0) }
.store(in: &cancellables)
② filter:过滤数据
Swift
Just(18)
.filter { $0 >= 18 } // 满足条件才往下发
.sink{ print("成年: \($0)") }
.store(in: &cancellables)
③ debounce:防抖(输入框神器)
- 停止输入 N 秒后才发值,避免频繁请求
Swift
let subject = PassthroughSubject<String, Never>()
subject
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink{ print("输入内容: \($0)") }
.store(in: &cancellables)
subject.send("h")
subject.send("he")
subject.send("hello") // 0.5秒后才收到
④ receive (on:):切换线程(UI 更新必备)
- 网络请求在子线程,更新 UI 必须切回主线程
Swift
URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main) // 切主线程更新UI
.sink{ _ in } receiveValue: { _ in }
⑤ flatMap:串联异步请求(先请求 A,再用 A 的结果请求 B)
Swift
// 先获取用户ID → 再用ID请求用户详情
fetchUserIdPublisher()
.flatMap { userId in fetchUserDetailPublisher(id: userId) }
.sink{ _ in }
5. Combine 网络请求(标准写法)
Swift
// 定义模型
struct User: Codable { let name: String }
let url = URL(string: "https://api.github.com/users/apple")!
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // 只取数据,忽略响应
.decode(type: User.self, decoder: JSONDecoder()) // 解码模型
.receive(on: DispatchQueue.main) // 切主线程
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("请求失败: \(error)")
}
},
receiveValue: { user in
print("用户名: \(user.name)")
}
)
.store(in: &cancellables)
6. Combine 必踩坑(细节!)
- 不存 cancellable → 订阅直接失效
- 错误事件会终止订阅(发一次错误,整个流结束)
- Subject 不 send (completion) → 内存泄漏
- UI 操作必须用 receive (on: .main)
第二部分:Swift 并发 超详细精讲(Swift5.5+,iOS15+)
核心定位
Swift 并发是原生结构化并发 ,专门处理一次性异步操作 (网络请求、耗时计算、并行任务),用 async/await 把异步代码写成同步风格,彻底消灭回调地狱。
1. 核心关键字(死记硬背)
| 关键字 | 作用 | 细节 |
|---|---|---|
| async | 标记异步函数 | 函数内部可以执行耗时操作,不阻塞线程 |
| await | 等待异步函数完成 | 只能在 async 上下文调用 |
| Task | 创建异步任务 | 把异步函数放到任务里执行 |
| TaskGroup | 并行执行多个任务 | 自动管理生命周期,支持取消 |
| Actor | 线程安全数据隔离 | 解决多线程数据竞争,自动加锁 |
| MainActor | 主线程执行 | UI 更新必须用 |
2. 最基础流程:async/await(逐行解析)
步骤 1:定义异步函数
Swift
// async 标记:这是一个异步函数,会耗时
func fetchUser() async throws -> User {
let url = URL(string: "https://api.github.com/users/apple")!
// await 等待:不阻塞线程,等网络请求完成
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
步骤 2:执行异步函数(必须包在 Task 里)
Swift
// Task:创建异步上下文,才能调用 await
Task {
do {
let user = try await fetchUser()
print("用户: \(user.name)")
} catch {
print("错误: \(error)")
}
}
✅ 这就是线性异步代码,没有任何嵌套!
3. Task 细节(必知)
① 取消任务
Swift
let task = Task {
try await fetchUser()
}
// 取消请求
task.cancel()
② 主线程 Task(UI 更新)
Swift
// @MainActor = 切主线程
Task { @MainActor in
label.text = "加载完成"
}
4. 并行任务:TaskGroup(核心高级用法)
场景:同时请求 3 个用户数据,等全部完成再汇总
Swift
func fetchAllUsers() async throws -> [User] {
// 任务组:管理多个并行异步任务
try await withThrowingTaskGroup(of: User.self) { group in
// 添加3个并行任务
group.addTask { try await fetchUser(id: 1) }
group.addTask { try await fetchUser(id: 2) }
group.addTask { try await fetchUser(id: 3) }
// 收集所有结果
var users = [User]()
for try await user in group {
users.append(user)
}
return users
}
}
✅ 自动并行、自动等待、自动取消、无内存泄漏
5. Actor:线程安全(解决多线程崩溃)
问题:多线程同时修改同一个变量 → 崩溃Actor 解决方案:自动隔离数据,同一时间只允许一个线程访问
Swift
// 定义Actor:线程安全容器
actor UserStore {
var users: [User] = []
func addUser(_ user: User) {
users.append(user)
}
}
// 使用:必须 await 调用
Task {
let store = UserStore()
await store.addUser(User(name: "A"))
}
✅ 无需手动加锁,绝对线程安全
6. Swift 并发 必踩坑
- async 函数不能直接调用 → 必须包
Task - await 只能在 async 上下文
- UI 必须用 @MainActor
- 错误必须用 do/catch 处理
第三部分:Combine ↔ Swift 并发 互操作(细节)
1. Combine → 并发(用 values)
所有 Publisher 都有 .values 属性,直接转异步序列
Swift
Task {
// Combine 发布者 → 异步序列
let publisher = Just("test")
for await value in publisher.values {
print(value)
}
}
2. 并发 → Combine(用 Future)
把 async 函数包成 Combine 发布者
Swift
func asyncToCombine() -> AnyPublisher<User, Error> {
Future { promise in
Task {
do {
let user = try await fetchUser()
promise(.success(user))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
第四部分:终极选型指南(什么时候用哪个?)
| 场景 | 选 Combine | 选 Swift 并发 |
|---|---|---|
| 输入框防抖、按钮点击、实时搜索 | ✅ | ❌ |
| 复杂数据流转换、串联请求 | ✅ | ❌ |
| 单次网络请求、耗时计算 | ❌ | ✅ |
| 并行执行多个任务 | ❌ | ✅ |
| 线程安全数据管理 | ❌ | ✅ |
| SwiftUI 状态绑定 | ✅ | ✅ |
总结(最细核心)
- Combine = 处理持续数据流,靠发布者 + 订阅者 + 操作符,必须持有 cancellable
- Swift 并发 = 处理一次性异步任务,靠 async/await/Task,结构化并发无泄漏
- 互操作 :Combine 转并发用
.values,并发转 Combine 用Future - 开发标配:UI 事件用 Combine,网络 / 并行用 Swift 并发