在 iOS 开发中,异步编程一直是一个绕不开的话题。从最初的 Block 回调,到 Combine 框架,再到现在的 async/await,Apple 一步步改进异步编程的方式,使代码更易读、更易维护。今天,我们就来聊聊 async/await 的优势,并深入探讨它如何帮助我们解决并发问题。
1. 回调地狱:异步编程的噩梦
在 iOS 早期,我们使用回调(Block)来处理异步任务,比如网络请求:
func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.global().async {
// 模拟网络请求
sleep(2)
let success = Bool.random()
if success {
completion(.success("数据加载成功"))
} else {
completion(.failure(NSError(domain: "网络错误", code: -1)))
}
}
}
虽然这样可以工作,但当任务依赖增多时,就会形成回调地狱:
fetchData { result in
switch result {
case .success(let data):
processData(data) { processedData in
saveToDatabase(processedData) { success in
if success {
print("数据处理完成")
}
}
}
case .failure(let error):
print("发生错误: \(error)")
}
}
层层嵌套的回调不仅使代码可读性变差,还增加了错误处理的复杂度。Apple 需要一种更优雅的方式。
2. Combine 的改进,但仍然复杂
Combine 通过声明式 API 改进了回调问题,使代码更加流畅:
fetchDataPublisher()
.map { processData($0) }
.flatMap { saveToDatabase($0) }
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("发生错误: \(error)")
}
}, receiveValue: { success in
print("数据处理完成: \(success)")
})
.store(in: &cancellables)
但 Combine 仍然需要繁琐的操作符,且新手入门成本较高。
3. async/await:更优雅的异步解决方案
Swift 5.5 引入了 async/await,使异步代码看起来像同步代码:
func fetchData() async throws -> String {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
if Bool.random() {
return "数据加载成功"
} else {
throw NSError(domain: "网络错误", code: -1)
}
}
func process() async {
do {
let data = try await fetchData()
print("数据处理完成: \(data)")
} catch {
print("发生错误: \(error)")
}
}
代码逻辑清晰、可读性高,没有回调地狱。
4. 并发问题:为什么需要 actor?
在引入 async/await 之前,我们使用 GCD 和 OperationQueue 进行并发处理。但并发访问共享资源时,可能会发生数据竞争,导致难以复现的 bug。例如:
class Counter {
var count = 0
func increment() {
count += 1
}
}
let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter.increment()
}
print(counter.count) // 可能不是 1000,出现数据竞争
原因在于值类型存储在栈上,线程独享;引用类型存储在堆上,多个线程共享,如果多个线程同时修改 count
,结果可能是不确定的。
Swift 通过actor 解决了这个问题。
5. actor:解决并发数据竞争
actor
是 Swift 引入的一种并发安全的引用类型,确保同一时间只有一个任务可以访问它的内部状态。
actor SafeCounter {
private var count = 0
func increment() {
count += 1
}
func getCount() -> Int {
return count
}
}
let counter = SafeCounter()
Task {
await counter.increment()
print(await counter.getCount())
}
在 actor
内部,Swift 自动确保并发访问安全,使代码更易维护。
6. SwiftUI 中的 Task 和 .task 修饰符
在 SwiftUI 中,我们可以使用 .task
修饰符启动异步任务:
struct ContentView: View {
@State private var data = "加载中..."
var body: some View {
Text(data)
.task {
do {
data = try await fetchData()
} catch {
data = "发生错误"
}
}
}
}
这样,视图在加载时会自动调用 fetchData()
,避免使用 onAppear {}
处理异步操作。
7. TaskGroup 处理多个异步任务
如果我们有多个并发任务需要执行,比如并行加载多张图片,可以使用 TaskGroup
:
func fetchMultipleData() async -> [String] {
await withTaskGroup(of: String.self) { group in
for i in 1...3 {
group.addTask {
return await fetchData() + "\(i)"
}
}
var results = [String]()
for await result in group {
results.append(result)
}
return results
}
}
TaskGroup
允许多个任务并行执行,并在所有任务完成后收集结果。
8. 总结
-
回调容易导致回调地狱,可读性差
-
Combine 解决了部分问题,但学习成本高
-
async/await 让代码更简洁,解决异步回调问题
-
actor 解决并发数据竞争,保证线程安全
-
SwiftUI
.task
让异步任务管理更方便 -
TaskGroup
适用于多个异步任务的并发处理
未来的 Swift 可能会继续演进并发模型,但目前 async/await 和 actor 已经是最佳实践,赶快用起来吧!