Swift并发编程:async/await实战,从崩溃到上架的血泪史
作者:一个从Android转Flutter、现在被迫学Swift的跨端苦命人
时间:2024年6月,凌晨2:17,刚刷完LeetCode第387题,咖啡快见底了
说实话,写这篇文章的时候,我正一边调试Flutter Web的CORS问题,一边准备下周的跳槽面试。最近公司接了个新活儿——给老iOS App做性能重构,而我这个“名义上的跨平台专家”,被产品经理一句“你不是会Swift吗?”直接推上了火线。
我:???
我是Android出身啊!虽然转Flutter后也摸过点SwiftUI,但并发?GCD?DispatchQueue?那都是上辈子的事了(其实是根本没系统学过)。结果上周五晚上,测试同学甩过来一个线上Crash日志:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
点进去一看,好家伙,主线程被一个异步网络回调卡死了,UI直接白屏。用户投诉量在App Store评论区飙升,运营小姐姐已经在群里@我三次了:“能不能今晚搞定?明天就是版本提审截止日!”
那一刻,我真的想砸MacBook——不是因为代码难写,是因为明明可以用async/await优雅解决的问题,为什么还要用回调地狱+锁+信号量搞出内存泄漏?
于是,熬了三个通宵,边查Apple文档边啃《Modern Concurrency in Swift》,总算把这套逻辑重构成了现代Swift并发模型。今天这篇技术分享,既是复盘,也是给正在准备iOS面试的兄弟们攒点干货——毕竟,“Swift并发”已经成了大厂iOS岗的高频面试题。
老代码的“屎山”现场
先看一眼旧实现(已脱敏):
class UserProfileManager {
private var cache: User?
private let queue = DispatchQueue(label: "com.myapp.user.profile", attributes: .concurrent)
func fetchUser(completion: @escaping (User?) -> Void) {
// 先查缓存
queue.sync {
if let cached = self.cache {
DispatchQueue.main.async {
completion(cached)
}
return
}
}
// 再发网络请求
NetworkService.shared.request(endpoint: .userProfile) { [weak self] result in
switch result {
case .success(let user):
self?.queue.async(flags: .barrier) {
self?.cache = user
}
DispatchQueue.main.async {
completion(user)
}
case .failure:
DispatchQueue.main.async {
completion(nil)
}
}
}
}
}
表面看没问题?但实际上:
- 线程切换混乱:
sync+async嵌套,主线程和后台队列来回横跳 - 竞态条件风险:多个调用同时触发时,缓存可能被重复写入
- 可读性极差:嵌套三层回调,新来的实习生看了直摇头
更致命的是,当网络慢、用户狂点刷新按钮时,queue.sync 在主线程执行会导致死锁——这正是那个EXC_BAD_ACCESS的元凶。
我当时就想问原作者:“兄弟,Apple在WWDC 2021就推出了async/await,你是不是还在用iPhone 6?”
拥抱现代Swift并发:async/await真香警告
Swift 5.5引入的并发模型,借鉴了JavaScript、C#甚至Kotlin的经验,核心就是三个关键词:
async:标记函数为异步await:等待异步结果Task:并发任务的基本单元
最重要的是——所有异步操作天然支持结构化并发(Structured Concurrency),自动管理生命周期,再也不用手动weak self防循环引用!
第一步:让网络层现代化
先把NetworkService改造成async版本:
// 现代化后的网络层
enum NetworkError: Error {
case invalidURL
case requestFailed
}
class NetworkService {
static let shared = NetworkService()
func request<T: Codable>(endpoint: APIEndpoint) async throws -> T {
guard let url = endpoint.url else { throw NetworkError.invalidURL }
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.requestFailed
}
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}
看,没有回调!没有completion handler!一行try await搞定。错误处理也回归了熟悉的do-catch,而不是满屏的.failure分支。
📌 小贴士:如果你还在用Alamofire,别慌!它从5.6版本开始也支持
async/await了,直接调用.serializingDecodable().value即可。
第二步:重构业务逻辑,告别GCD
现在重写UserProfileManager:
actor UserProfileManager {
private var cache: User?
func fetchUser() async throws -> User {
// 1. 先查缓存(actor保证线程安全)
if let cached = cache {
return cached
}
// 2. 缓存未命中,发起网络请求
let user = try await NetworkService.shared.request(endpoint: .userProfile)
// 3. 更新缓存(actor内部自动同步)
cache = user
return user
}
}
重点来了:这里用了actor!
在Swift并发模型中,actor是专门用来保护可变状态的类型。它保证同一时间只有一个任务能访问其内部属性,彻底解决了数据竞争问题——再也不用手动加锁、信号量、barrier了!
对比一下新旧方案:
| 特性 | 传统GCD方案 | 现代async/await + actor |
|---|---|---|
| 线程安全 | 手动管理,易出错 | 自动保证,编译器检查 |
| 代码可读性 | 回调地狱,嵌套深 | 线性流程,像同步代码 |
| 错误处理 | 分散在回调中 | 集中式do-catch |
| 内存管理 | 需weak self防循环引用 |
结构化并发自动释放 |
| 调试难度 | 断点难打,日志混乱 | 直接单步调试 |
看到没?这就是生产力的代差。
实战:处理复杂场景——并发请求与超时控制
当然,真实项目哪有这么简单。比如我们有个需求:同时拉取用户资料、订单列表、消息通知,全部成功才更新UI,任一失败则走降级逻辑。
以前的做法可能是用DispatchGroup:
let group = DispatchGroup()
var user: User?
var orders: [Order]?
var notifications: [Notification]?
group.enter(); fetchUser { u in user = u; group.leave() }
group.enter(); fetchOrders { o in orders = o; group.leave() }
group.enter(); fetchNotifications { n in notifications = n; group.leave() }
group.notify(queue: .main) {
if let u = user, let o = orders, let n = notifications {
updateUI(u, o, n)
} else {
showDegradedUI()
}
}
现在?一行代码搞定:
func loadDashboard() async {
do {
// 并发执行三个任务
async let user = userManager.fetchUser()
async let orders = orderManager.fetchOrders()
async let notifications = notificationManager.fetchNotifications()
// 等待全部完成
let (u, o, n) = try await (user, orders, notifications)
await MainActor.run {
updateUI(u, o, n)
}
} catch {
await MainActor.run {
showDegradedUI()
}
}
}
async let是Swift并发的魔法语法——它会自动并发执行右边的异步调用,并在await时收集结果。失败任何一个,整个try await就会抛出异常。
加个超时控制?
有时候网络特别烂,不能让用户无限等。传统做法得结合DispatchSourceTimer,现在只需:
func fetchWithTimeout() async throws -> User {
let fetchTask = Task {
try await userManager.fetchUser()
}
let timeoutTask = Task {
try await Task.sleep(nanoseconds: 5_000_000_000) // 5秒
throw NetworkError.timeout
}
// 谁先完成就返回谁
return try await withTaskGroup(of: Result<User, Error>.self) { group in
group.addTask { Result(catching: { try await fetchTask.value }) }
group.addTask { Result(catching: { try await timeoutTask.value }) }
let result = try await group.next()!
return try result.get()
}
}
(其实更优雅的方式是用withUnsafeContinuation封装超时逻辑,但为了演示原理先这么写)
与SpringBoot后端的协同思考
说到这里,突然想到我们后端是用SpringBoot写的。有意思的是,Swift的async/await和Java的CompletableFuture + Project Reactor在理念上高度一致——都是响应式、非阻塞、背压感知。
我们前后端约定:所有API默认超时5秒,支持ETag缓存。于是我在Swift端加了一层智能缓存:
actor SmartCache<T: Codable & Equatable> {
private var value: T?
private var etag: String?
func getOrFetch(
from url: URL,
etag: String? = nil
) async throws -> T {
// 如果已有缓存且ETag匹配,直接返回
if let cached = value, self.etag == etag {
return cached
}
// 否则发起带If-None-Match头的请求
var request = URLRequest(url: url)
if let currentEtag = self.etag {
request.setValue(currentEtag, forHTTPHeaderField: "If-None-Match")
}
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 304 {
// 304 Not Modified,返回缓存
return value!
}
// 解析新数据
let newValue = try JSONDecoder().decode(T.self, from: data)
self.value = newValue
self.etag = response.value(forHTTPHeaderField: "ETag")
return newValue
}
}
这样,一次完整的用户资料加载,平均耗时从820ms降到210ms(数据来自Xcode Instruments实测)。产品总监看到报告后,居然主动请我喝了杯瑞幸——这在我司可是稀罕事!
App Store审核踩坑记
本以为万事大吉,结果提审被拒了两次。
第一次拒绝理由:
“App在后台时频繁唤醒网络,违反App Store Review Guideline 2.5.4”
查了半天,发现是某个Task在SceneDelegate里没取消。解决方案:用TaskGroup或手动cancel()。
第二次拒绝理由:
“使用私有API:_asyncMainDrain”
这其实是Xcode 14.3的一个bug,升级到14.3.1就解决了。
最后学乖了:所有异步任务都绑定到生命周期:
class ProfileViewController: UIViewController {
private var fetchTask: Task<Void, Never>?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchTask = Task {
await loadProfile()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
fetchTask?.cancel()
}
}
记住:Task不是万能的,记得在适当时候cancel()!
写在最后:代码人生,不止于技术
回看这段经历,从被逼上梁山到顺利上架,我最大的感悟是:
技术栈可以跨界,但工程思维必须扎实。
无论是Android的Handler、Flutter的Isolate,还是Swift的actor,本质都是在解决并发、状态、副作用这三个永恒命题。而async/await之所以成为行业标准,正是因为它用最接近人类思维的方式表达了异步流程。
现在,每当我深夜刷题累了,就会打开Xcode看看那段清爽的并发代码——没有回调地狱,没有线程锁,只有清晰的逻辑流。那一刻,我觉得自己不只是个码农,更像是个用代码写诗的人。
当然,现实是:明天还要继续改Flutter的Web兼容性问题,后天要面字节跳动……但至少今晚,我睡得着了。
面试题彩蛋
如果你正在准备iOS面试,这几个async/await相关问题大概率会被问到:
actor和class有什么区别?如何选择?Task.detached和普通Task的区别是什么?- 如何在
async函数中调用同步的第三方库(如旧版Alamofire)? MainActor的作用是什么?为什么UI更新必须用它?- 结构化并发(Structured Concurrency)解决了哪些传统并发问题?
建议结合本文代码,自己动手写一遍。毕竟,talk is cheap, show me the code。
作者碎碎念:
这篇文章写了4小时,期间重启了3次Xcode(你懂的)。
如果你觉得有用,欢迎点赞/转发,让我知道我不是一个人在战斗。
下期预告:《从Flutter到SwiftUI:一个跨端程序员的UI哲学》—— 一个在代码和面试题之间反复横跳的打工人

评论 0