Swift并发编程:async/await实战 —— 一个运维老狗的iOS并发踩坑记

远方的接口
2025-12-17 18:09
阅读 290

上周五晚上十一点,我正窝在工位上一边啃着冷掉的黄焖鸡,一边盯着 Jenkins 上跑飞的 CI 流水线——又是某个 iOS 团队提了个“微小改动”,结果把整个 App 的网络层搞崩了。产品经理在 Slack 里疯狂@我:“线上用户打不开首页!是不是你们 DevOps 又改了什么配置?” 我翻了个白眼:兄弟,这回真不是我们锅,是你们自己写的异步回调嵌套八层,连 Swift 编译器都看不下去了。

作为在这家公司待了三年多的 DevOps 工程师(没错,就是那个天天跟 Ansible、K8s、Prometheus 打交道,Vim 快捷键比吃饭还熟的老运维),最近却被迫跨界研究起了 Swift 并发模型。原因?很简单——想跳槽。最近投了几家大厂,面试官张口就问:“说说 Swift 的 async/await 和 GCD 的区别?”、“你们项目里怎么处理 Task 取消和资源泄漏?”、“如果用 SwiftUI + async/await 做一个实时数据流页面,你会怎么设计?”

我当时就懵了:我可是写 Shell 脚本出身的啊!但转念一想,现在 DevOps 和 SRE 都要懂应用层逻辑了,尤其 Apple 生态越来越封闭,不懂点 iOS 开发,连日志都看不懂。于是,我硬着头皮啃文档、翻 WWDC 视频,还在公司内部拉了个“Swift 并发攻坚小组”(其实就是我和 iOS 组两个实习生一起 debug 到凌晨三点)。

今天这篇文章,就是我从一个运维视角,结合真实项目踩坑经验,手把手带你用 Swift 的 async/await 重构一个高并发、低延迟的 App 功能。别担心,我不是 iOS 专家,但正因为“外行”,我才更清楚哪些地方容易栽跟头。


为什么不用 GCD?因为回调地狱真的会死人

先说背景。我们 App 有个“个人中心”页面,要同时加载:

  • 用户基本信息(调 /user/profile
  • 最近订单列表(调 /orders/recent
  • 推荐商品(调 /recommendations

旧代码是这么写的(简化版):

func loadUserData() {
    NetworkService.fetchProfile { profile in
        DispatchQueue.main.async {
            self.profile = profile
            NetworkService.fetchOrders { orders in
                DispatchQueue.main.async {
                    self.orders = orders
                    NetworkService.fetchRecommendations { recs in
                        DispatchQueue.main.async {
                            self.recommendations = recs
                            self.isLoading = false
                        }
                    }
                }
            }
        }
    }
}

看到没?三重回调嵌套,缩进都快贴到屏幕右边了。更惨的是,如果任何一个接口失败,整个链路就断了,而且没法并行请求——明明三个 API 互不依赖,却要串行等 3 秒。产品经理说“首屏要 1 秒内打开”,我心想:你行你上。

这时候,async/await 就成了救命稻草。它不是魔法,但能让你用同步的写法,写出异步的逻辑,而且天然支持结构化并发(Structured Concurrency)——这是 Apple 在 Swift 5.5 引入的核心特性。


重构第一步:把回调函数改成 async 函数

首先,得把底层网络层改造一下。假设你用的是 URLSession(没用 Alamofire,因为我们团队信奉“少引入一个依赖就少一个坑”),原来的回调式方法:

static func fetchProfile(completion: @escaping (Profile?) -> Void) {
    let url = URL(string: "https://api.example.com/user/profile")!
    URLSession.shared.dataTask(with: url) { data, _, _ in
        let profile = try? JSONDecoder().decode(Profile.self, from: data!)
        completion(profile)
    }.resume()
}

改成 async throws 版本:

static func fetchProfile() async throws -> Profile {
    let url = URL(string: "https://api.example.com/user/profile")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Profile.self, from: data)
}

关键点:

  • async 表示这个函数会挂起(suspend),不会阻塞主线程
  • throws 表示可能抛异常,调用方必须用 try 处理
  • URLSession.shared.data(from:) 是系统提供的 async 版本,不用自己包装

📌 运维视角吐槽:这种改造其实和我们在后端用 Springboot 写 CompletableFuture 或 Kotlin 协程很像——都是把“回调地狱”变成“线性流程”。只不过 Swift 把并发模型直接集成到语言层面,不用像 Java 那样依赖框架。


实战:用 TaskGroup 实现真正的并行请求

现在,主逻辑可以重写了:

@MainActor
class UserProfileViewModel: ObservableObject {
    @Published var profile: Profile?
    @Published var orders: [Order] = []
    @Published var recommendations: [Product] = []
    @Published var isLoading = false

    func loadData() async {
        isLoading = true
        defer { isLoading = false }

        do {
            // 三个请求并行执行!
            async let profileTask = NetworkService.fetchProfile()
            async let ordersTask = NetworkService.fetchOrders()
            async let recsTask = NetworkService.fetchRecommendations()

            // 等待所有结果
            let profile = try await profileTask
            let orders = try await ordersTask
            let recs = try await recsTask

            self.profile = profile
            self.orders = orders
            self.recommendations = recs
        } catch {
            print("Load failed: $error)")
            // 这里可以加 Sentry 上报,运维最爱
        }
    }
}

注意几个细节:

  • @MainActor 确保所有 UI 更新都在主线程(SwiftUI 要求)
  • async let 是 Swift 并发的黑科技——声明即启动,并行执行
  • 所有 await 放在一起,逻辑清晰,还能自动处理错误传播

性能提升立竿见影:原本 3 秒的串行请求,现在 1.2 秒搞定(取决于最慢的那个 API)。App Store 审核时,苹果特别看重“启动速度”和“响应流畅度”,这种优化直接加分。


避坑指南:那些让我想砸键盘的并发 Bug

坑 1:忘记取消 Task,导致内存泄漏

有一次,用户快速切换页面,旧的 loadData() 还在后台跑,新页面又启动了新的请求。结果内存飙升,Xcode Instruments 一查,NetworkService 实例堆积如山。

解决方案:用 Task 显式管理生命周期。

class UserProfileView: View {
    @StateObject private var viewModel = UserProfileViewModel()
    @State private var loadDataTask: Task<Void, Never>? = nil

    var body: some View {
        // ...
            .onAppear {
                loadDataTask?.cancel() // 先取消旧任务
                loadDataTask = Task {
                    await viewModel.loadData()
                }
            }
            .onDisappear {
                loadDataTask?.cancel()
            }
    }
}

💡 运维经验:这就像我们在 K8s 里用 preStop hook 优雅终止 Pod 一样——资源必须显式释放,否则迟早 OOM。

坑 2:在非 MainActor 里更新 @Published 变量

Swift 5.7 开始严格检查 Actor 隔离。如果你在后台 Task 里直接写 self.profile = xxx,编译器会报错:

Mutation of captured var 'profile' in concurrently-executing code

正确做法:要么用 @MainActor 标注整个 ViewModel,要么在赋值前切回主线程:

await MainActor.run {
    self.profile = profile
}

坑 3:测试时卡死——别在 XCTestCase 里直接 await

单元测试里这样写会 hang 住:

func testLoadData() async {
    await viewModel.loadData() // ❌ 可能死锁
}

正确姿势:用 XCTestExpectation + Task 包裹:

func testLoadData() {
    let expectation = XCTestExpectation(description: "Load complete")
    Task {
        await viewModel.loadData()
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 5)
}

和 Springboot 对比?其实思路一脉相承

虽然我是搞后端运维的,但发现 Swift 并发和 Java 生态很像:

场景 Swift Springboot (Java)
异步函数 func foo() async throws -> T CompletableFuture<T> foo()
并行执行 async let a = ..., b = ... CompletableFuture.allOf(a, b)
错误处理 do-catch .exceptionally()
主线程安全 @MainActor @Async + 主线程回调

所以如果你熟悉 Springboot 的异步编程,学 Swift 并发会快很多。反过来,面试官问“Swift 并发模型和 Java 虚拟线程有什么异同?”,你也能侃两句——这可是高级工程师加分项。


上架 App Store 时的并发注意事项

Apple 对后台任务审核很严。如果你在 SceneDelegateAppDelegate 里启动了长期运行的 Task,记得:

  1. 不要滥用 Task.detached:它脱离当前上下文,容易导致资源泄漏
  2. 网络请求必须可取消:审核指南明确要求“用户离开页面后应停止加载”
  3. 避免在后台刷新中做 heavy workBGProcessingTask 有严格时间限制

我们之前有个版本因为没处理 Task 取消,被拒了两次。理由是:“App 在后台持续消耗 CPU”。后来加上 task.isCancelled 检查才过审。

func backgroundSync() async {
    while !Task.isCancelled {
        await fetchData()
        try? await Task.sleep(nanoseconds: 5_000_000_000) // 5秒
    }
}

总结:async/await 不是银弹,但值得拥抱

经过这次重构,我们 App 的首屏加载时间从 2.8s 降到 1.1s,Crash 率下降 40%(主要是因为不再乱 dispatch 到主线程了)。更重要的是,代码可读性爆炸提升——现在实习生都能看懂数据加载逻辑,再也不用问我“这个 completion block 是第几层了?”

作为一个 Vim 党、Shell 脚本狂魔,我曾经觉得 iOS 开发花里胡哨。但深入 Swift 并发后才发现,Apple 这次真的把工程化做到了极致:结构化并发 + Actor 模型 + 严格的编译期检查,几乎从根源上杜绝了竞态条件和回调地狱。

如果你也在准备跳槽,或者被产品经理逼着优化性能,async/await 绝对是必学技能。网上教程很多(推荐 WWDC21 的《Meet async/await in Swift》),但光看不行,一定要在真实项目里踩坑。就像我们 DevOps 说的:“没在生产环境炸过,不算真学会。”

最后送大家一句运维老狗的忠告:并发不是炫技,而是为了写出更稳定、更可维护的代码。毕竟,半夜三点被 PagerDuty 叫醒修 Bug 的,可是你自己。

(完)

P.S. 如果你正在找 DevOps 或 SRE 岗,且团队愿意让我偶尔写写 Swift,欢迎私信!我已经受够了每天解释“为什么 iOS 发版要等 QA 三天” 😅

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝