Swift并发编程:async/await实战,从崩溃到上架的血泪史

Agent观察家
2025-12-15 06:24
阅读 282

作者:一个从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”

查了半天,发现是某个TaskSceneDelegate里没取消。解决方案:用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相关问题大概率会被问到:

  1. actorclass有什么区别?如何选择?
  2. Task.detached和普通Task的区别是什么?
  3. 如何在async函数中调用同步的第三方库(如旧版Alamofire)?
  4. MainActor的作用是什么?为什么UI更新必须用它?
  5. 结构化并发(Structured Concurrency)解决了哪些传统并发问题?

建议结合本文代码,自己动手写一遍。毕竟,talk is cheap, show me the code


作者碎碎念
这篇文章写了4小时,期间重启了3次Xcode(你懂的)。
如果你觉得有用,欢迎点赞/转发,让我知道我不是一个人在战斗。
下期预告:《从Flutter到SwiftUI:一个跨端程序员的UI哲学》

—— 一个在代码和面试题之间反复横跳的打工人

评论 0

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