Swift 并发迁移手记:从回调到 async/await
上个月把手头一个老项目的网络层从回调地狱里捞了出来,全面改成 async/await。过程比想象中顺利,但有几个坑值得记下来,下次就不用再查半天了。
坑一:@MainActor 的传染性
给 ViewModel 加了 @MainActor 之后,所有调用它的方法都被迫隔离到主线程,包括几个纯粹做数据清洗的函数。结果是列表滑动时偶尔掉帧。解决办法是把清洗逻辑拆到独立的 actor 里,ViewModel 只负责最后一步赋值。
坑二:Sendable 检查半夜报错
CI 里开了完整并发检查之后,一批以前能过的代码开始报 Capture of non-Sendable type。多数情况是模型类没标 final 或者属性可变。我的处理原则很简单:模型一律改 struct,真需要 class 的就标 @unchecked Sendable 并写清楚为什么安全,而不是把检查关掉。
坑三:桥接老回调 API
少数第三方回调没有 async 版本,用 withCheckedContinuation 包一层就行,但一定要保证 continuation 只 resume 一次。调试时在闭包里加个断言,比上线后收到诡异崩溃强得多:
func load() async throws -> Data {
try await withCheckedThrowingContinuation { cont in
legacyFetch { result in
cont.resume(with: result)
}
}
}
总的来说,迁移完最大的感受不是代码变短了,而是错误处理终于回到了一条直线上。值得。