Swift并发编程:async/await实战踩坑实录
上周五晚上十点半,办公室只剩我和隔壁组的运维小哥。他蹲在机房门口啃煎饼果子,我盯着Xcode里一堆DispatchQueue回调嵌套发呆——这代码简直像意大利面拌了胶水,又乱又黏。就在产品经理第N次催问“用户头像加载能不能再快点”时,我突然想起WWDC21那个被我们全组忽略的Swift并发模型。第二天顶着黑眼圈翻文档,才惊觉:这玩意儿简直是为游戏服务端转iOS开发的我量身定制的!
作为在网易做了三年服务端的老兵(主要搞后端逻辑和网络协议),去年调岗到新成立的移动端小组时,最不适应的就是iOS那套异步回调地狱。写惯了Go的goroutine和Java的CompletableFuture,看到Objective-C时代遗留的completion handler嵌套三层以上就想砸键盘。好在Swift 5.5带来的async/await终于让iOS开发有了现代语言的样子。
被面试题逼出来的学习契机
事情要从上个月说起。组里来了个实习生,拿着LeetCode刷题准备秋招。有天他神秘兮兮地问我:“哥,Swift怎么处理异步操作?能像JavaScript那样用async/await吗?” 我当时正忙着修一个因线程竞争导致的游戏道具丢失bug(别问,问就是没加锁),随口回了句“iOS哪有那么高级”。结果第二天晨会,技术总监老王直接甩出一道面试题挑战:
“实现一个带缓存的图片加载器,要求:1) 网络请求失败自动重试3次 2) 相同URL的请求要复用结果 3) 主线程不能阻塞”
看着实习生憋红的脸,我默默打开了Apple官方文档。这一看不得了——Swift的并发模型居然和JS的Promise有异曲同工之妙,但更安全!特别是Task和Actor的设计,完美解决了我在服务端最头疼的数据竞争问题。
从回调地狱到结构化并发
先说说传统写法有多反人类。假设我们要加载用户头像并更新UI:
// 地狱模式(别学!)
NetworkManager.shared.fetchImage(url: avatarURL) { image, error in
guard let image = image else {
// 处理错误...这里可能又要嵌套
return
}
DispatchQueue.main.async {
self.imageView.image = image
// 如果还要加载背景图?再嵌套一层!
}
}
这种代码在我刚接手的项目里随处可见,测试同学每次提bug都说“偶现图片不显示”,八成是主线程切换时机出了问题。而用async/await重构后:
// 天堂模式(真香!)
@MainActor
func loadAvatar() async {
do {
let image = try await NetworkManager.shared.fetchImage(url: avatarURL)
imageView.image = image // 自动在主线程执行!
} catch {
handleError(error)
}
}
关键点在于@MainActor这个属性包装器——它保证了函数体内所有代码都在主线程执行,再也不用担心跨线程访问UI控件被Crash Reporter追着打。这设计思路和我们服务端用Actor模型隔离状态简直一模一样!
实战:打造高可靠图片加载器
回到面试题挑战,我花了三天时间用Swift并发模型重写了图片加载模块。核心思路分三步走:
第一步:基础网络请求封装
// NetworkManager.swift
class NetworkManager {
func fetchImage(url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
注意这里直接抛异常而不是回调,配合try await使用清爽得不行。不过第一次上线就翻车了——测试同学反馈弱网环境下App卡死。原来默认的URLSession超时太短,赶紧加上配置:
private lazy var session: URLSession = {
var config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 10 // 关键!
return URLSession(configuration: config)
}()
第二步:实现智能重试机制
重试逻辑用递归最直观:
func fetchWithRetry(url: URL, maxRetries: Int = 3) async throws -> UIImage {
do {
return try await fetchImage(url: url)
} catch {
if maxRetries > 0 {
print("重试剩余次数: \(maxRetries)")
return try await fetchWithRetry(url: url, maxRetries: maxRetries - 1)
}
throw error // 抛出最终错误
}
}
但要注意避免无限递归!有次我手滑把maxRetries - 1写成maxRetries + 1,模拟器直接内存爆炸——还好是在开发阶段,不然上架审核肯定被拒(App Store对崩溃率可是零容忍)。
第三步:请求去重与缓存
这才是最棘手的部分。多个页面同时请求同一张头像时,必须合并请求。最初想用字典存Task,但发现Swift的Task没有类似JS Promise的“pending状态共享”机制。灵机一动想到用AsyncStream:
actor ImageCache {
private var inflightTasks: [URL: Task<UIImage, Error>] = [:]
func loadImage(url: URL) async throws -> UIImage {
// 检查是否有进行中的任务
if let task = inflightTasks[url] {
return try await task.value
}
// 创建新任务
let task = Task {
defer {
Task { @MainActor in
inflightTasks[url] = nil // 注意Actor隔离
}
}
return try await fetchWithRetry(url: url)
}
inflightTasks[url] = task
return try await task.value
}
}
这里用了actor来保证缓存字典的线程安全——这正是我在服务端天天念叨的“状态隔离”思想!不过要注意defer块里修改actor属性必须用Task { @MainActor in ... },否则编译器会报错。第一次写的时候被这个坑得够呛,Xcode提示“Mutating actor state from non-isolated context”时我还以为是语法错误。
JS开发者特别注意事项
组里有个前端转iOS的同事总把Swift并发和JS搞混,这里划重点:
| 特性 | JavaScript (ES2017+) | Swift (5.5+) |
|---|---|---|
| 异常处理 | try/catch | try/catch |
| 并发模型 | 单线程事件循环 | 结构化并发(Task/Actor) |
| 状态共享 | 需手动加锁 | Actor自动隔离 |
| 取消机制 | AbortController | Task.cancel() |
| 主线程保障 | 无(需手动dispatch) | @MainActor自动处理 |
最大的区别在于Swift的并发是编译期保障的。比如你试图在非MainActor里改UI,Xcode直接标红报错;而在JS里可能运行时才崩。这对从服务端转过来的我简直是福音——再也不用担心半夜被PagerDuty叫醒修数据竞争bug了!
上线后的血泪教训
本以为万事大吉,结果上架前两天遇到审核被拒。苹果审核员反馈:“App在后台时触发网络请求”。排查发现是某个定时任务没检查应用状态:
// 错误示范!
Task {
let data = try await fetchData() // 后台也会执行
}
// 正确姿势
Task {
guard UIApplication.shared.applicationState == .active else {
return // 后台直接退出
}
let data = try await fetchData()
}
另外要注意iOS 13以下系统不支持async/await!我们游戏最低支持iOS 12,最后不得不保留两套网络层。建议新项目直接放弃老系统,毕竟现在iOS 15+覆盖率都90%了。
给同行的建议
如果你和我一样从服务端转移动端,或者正被回调地狱折磨,强烈建议拥抱Swift并发:
- 从简单场景入手:先改造工具类方法(比如文件读写),别一上来就重构核心模块
- 善用Xcode诊断工具:打开Thread Sanitizer能提前发现数据竞争
- 别滥用@MainActor:只在真正需要主线程的地方用,否则会拖慢性能
- 测试弱网环境:用Network Link Conditioner模拟2G网络,很多并发bug只在弱网出现
最后说个好消息:自从用上async/await,我们项目的Crash率下降了40%,连测试同学都夸代码好测了(虽然她还是经常找我修bug)。上周团建吃饭时,产品经理举杯说:“下次需求能不能周三给?周五上线那种...” 我默默掏出手机看了眼日历——还好这次不用再熬夜改回调了!
作者注:本文代码已在GitHub开源(链接略),包含完整的单元测试用例。作为Vim党,我甚至写了套async/await的snippets,用
:AsyncFetch就能生成模板代码——谁说老派程序员不能拥抱新技术?

评论 0