1. 为什么需要 async/await
在移动开发里,"并发/异步"几乎无处不在:网络请求、图片下载、文件读写、数据库操作......它们都有一个共同特点:
- 耗时(如果你在主线程里死等,会卡 UI)
- 结果稍后才回来(你必须用某种方式"拿到结果")
传统的并发模型大多是"回调式"的:用 completion、delegate、通知等在未来某个时间点把结果交还给你。 这套方案能跑,但会带来两个典型挑战(你参考资料里也点出来了):
- 代码维护性差:异步回调让代码阅读"跳来跳去",不线性
- 容易出现数据竞争(Data Races):共享可变状态在并发下很容易出 Bug,而且难复现、难排查
Swift 从 5.5 开始引入新并发框架,async/await 先解决第一个核心痛点:让异步代码看起来像同步代码一样顺畅,并且把错误处理变得更自然。
2. 先复习 4 个基础概念
2.1 同步 vs 异步(描述"函数怎么返回")
- 同步(Synchronous) :函数执行完才返回 例子:
let x = add(1, 2),拿到结果才能往下走 - 异步(Asynchronous) :函数把任务"丢出去",结果以后再给你 所以你必须用
completion/ delegate / async/await 等方式拿结果
2.2 串行 vs 并行/并发(描述"一组任务怎么跑")
- 串行(Serial):一次只执行一个任务,完成一个再下一个
- 并发/并行(Concurrent/Parallel):同一段时间内可能执行多个任务 (本文不严格区分并发与并行;你只要知道:会"同时处理多个任务"就行)
3. 回调式异步的典型痛点:回调地狱 + 错误处理难看
用"头像加载"来说明: 步骤:拿 URL → 下载数据(加密)→ 解密 → 解码成图片
3.1 回调地狱(Callback Hell)
swift
class AvatarLoader {
func loadAvatar(token: String, completion: @escaping (UIImage) -> Void) {
fetchAvatarURL(token: token) { url in
self.fetchAvatar(url: url) { data in
self.decryptAvatar(data: data) { data in
self.decodeImage(data: data) { image in
completion(image)
}
}
}
}
}
func fetchAvatarURL(token: String, completion: @escaping (String) -> Void) { /* ... */ }
func fetchAvatar(url: String, completion: @escaping (Data) -> Void) { /* ... */ }
func decryptAvatar(data: Data, completion: @escaping (Data) -> Void) { /* ... */ }
func decodeImage(data: Data, completion: @escaping (UIImage) -> Void) { /* ... */ }
}
阅读体验像在"走迷宫":你要不停地进回调、出回调,脑子要同时记住很多上下文。
3.2 错误处理会让代码更糟
异步回调常见的写法是 completion(value, error) 或者 Result。 但在多层嵌套里,错误传递会越来越啰嗦,漏掉某个分支的 completion 也很常见。
4. async/await 的核心:把"异步代码"写成"线性代码"
4.1 先看效果:一眼就能读懂
swift
class AvatarLoader {
func loadAvatar(token: String) async throws -> UIImage {
let url = try await fetchAvatarURL(token: token)
let encryptedData = try await fetchAvatar(url: url)
let decryptedData = try await decryptAvatar(data: encryptedData)
return try await decodeImage(data: decryptedData)
}
func fetchAvatarURL(token: String) async throws -> String { /* ... */ }
func fetchAvatar(url: String) async throws -> Data { /* ... */ }
func decryptAvatar(data: Data) async throws -> Data { /* ... */ }
func decodeImage(data: Data) async throws -> UIImage { /* ... */ }
}
你会发现:
- 代码像同步一样从上到下执行("直线式")
- 错误处理回到了熟悉的
throws + try + do/catch - 逻辑清晰、可维护性大幅提升
5. 语法规则:你只需要掌握这 3 条
5.1 async:声明"这个函数可能会挂起"
swift
func fetch() async -> Int { 42 }
含义:它是"异步函数",执行过程中可能暂停(挂起),等待某些事情完成(比如网络返回、IO 完成)。
5.2 await:调用 async 函数时必须写
swift
let value = await fetch()
await 表示:这里是一个潜在挂起点(potential suspension point)。 意思是:运行到这里,可能会"先停一下",等结果准备好再继续往下走。
5.3 await 只能出现在"异步上下文"里
异步上下文主要有两类:
- async 函数体内部
- Task 闭包内部
6. 一个关键认知:挂起的是"函数",不是"线程"
这是很多新手最容易误解的地方:
await不是"把当前线程卡住等待"- 它是"把当前函数挂起",让出执行权
- 等条件满足后,再恢复执行(恢复时可能换了线程)
你可以把它想象成:
你在排队取号(await),你人可以先离开去做别的(线程去执行别的任务),等叫到你号了你再回来继续办理(函数恢复执行)。
结论:在 async/await 的世界里,别强依赖"我现在一定在某个线程上"。
7. 为什么"锁 + await"容易出事:一个经典坑
一个很典型的示例:
swift
let lock = NSLock()
func test() async {
lock.lock()
try? await Task.sleep(nanoseconds: 1_000_000_000)
lock.unlock()
}
for _ in 0..<10 {
Task { await test() }
}
问题在哪里?
- 在
lock.lock()后遇到了await - 函数可能挂起并发生线程切换
- 其他任务也想拿锁,但锁可能被"拿着不放"
- 结果就是:很容易出现死锁/饥饿等难排查问题
经验法则:
不要在持有锁的期间跨过
await。 如果你需要保护共享可变状态,优先考虑actor或让状态只在单一执行上下文里修改。
8. 真正能上手的代码:用 URLSession 写一个 async 网络请求
iOS 15+(Swift 5.5+)开始,URLSession 已经提供了 async API,例如:
swift
let (data, response) = try await URLSession.shared.data(from: url)
我们做一个"获取用户信息"的示例(包含错误处理):
8.1 定义模型与错误
swift
import Foundation
struct User: Decodable {
let id: Int
let name: String
}
enum NetworkError: Error {
case invalidURL
case invalidResponse
case httpStatus(Int)
}
8.2 写一个 async 网络层
swift
final class APIClient {
func fetchUser(id: Int) async throws -> User {
guard let url = URL(string: "https://example.com/users/\(id)") else {
throw NetworkError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(http.statusCode) else {
throw NetworkError.httpStatus(http.statusCode)
}
return try JSONDecoder().decode(User.self, from: data)
}
}
这一段代码的阅读体验就是"同步式"的:构建 URL → await 拿数据 → 校验 → decode。
9. 在 UIViewController 里怎么调用 async 并更新 UI?
这是 iOS 开发里最常见的落地问题:
async 方法不能直接在普通函数里
await,那我怎么从按钮点击里发请求?
答案:用 Task { } 把它放进异步上下文里。
9.1 示例:点击按钮加载用户并刷新 label
swift
import UIKit
final class UserViewController: UIViewController {
private let api = APIClient()
private let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
label.numberOfLines = 0
label.frame = CGRect(x: 20, y: 100, width: 320, height: 200)
view.addSubview(label)
loadUser()
}
private func loadUser() {
// 进入异步上下文
Task { [weak self] in
guard let self else { return }
do {
let user = try await api.fetchUser(id: 1)
// 更新 UI:回到主线程(MainActor)
await MainActor.run {
self.label.text = "用户:\(user.name)\nID:\(user.id)"
}
} catch {
await MainActor.run {
self.label.text = "加载失败:\(error)"
}
}
}
}
}
你只要记住一句话就够了:
- 耗时工作 放在
Task里await - UI 更新 放在
MainActor(主线程语义)里做
10. 从旧代码迁移:把 completion 回调包装成 async
很多项目里已经有大量回调式 API,你不可能一夜之间全改掉。Swift 提供了"续体(Continuation)"做桥接。
10.1 假设你有旧接口
swift
final class LegacyService {
func fetchText(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion(.success("hello async/await"))
}
}
}
10.2 用 withCheckedThrowingContinuation 包装
swift
extension LegacyService {
func fetchText() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
fetchText { result in
switch result {
case .success(let text):
continuation.resume(returning: text)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
10.3 使用方式
swift
Task {
do {
let text = try await LegacyService().fetchText()
print(text)
} catch {
print("error:", error)
}
}
11. 入门阶段的最佳实践清单
- 不要在持锁期间跨
await(容易死锁/逻辑卡住) - UI 更新统一回到
MainActor(避免主线程问题) - 能用
throws就别用 Optional+error 乱传:错误路径更清晰 - 从入口处就结构化 :
async函数调用async函数,别层层回调 - 迁移旧代码用 Continuation:逐步改,不要一次性重构到崩
12. 小结
到这里,你已经具备了 async/await 的"可用级理解":
async:这个函数可能挂起await:潜在挂起点,只能在异步上下文使用- async/await 让异步代码"线性可读",错误处理回到
throws - 挂起的是函数,不是线程;
await前后可能换线程 - iOS 里用
Task {}进入异步上下文,用MainActor更新 UI - 旧回调接口可以用
withCheckedThrowingContinuation平滑迁移