iOS Swift Concurrency:用 async/await 优化网络请求代码

Swift 5.5 引入的 async/await 彻底改变了异步编程范式,特别适合优化网络请求代码。以下是如何使用它替代传统回调模式:

1. 传统回调模式的问题

传统方式使用闭包处理异步结果,易导致"回调地狱":

func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: userURL) { data, _, error in
        if let error = error { 
            completion(.failure(error))
            return
        }
        guard let data = data else { 
            completion(.failure(NetworkError.noData))
            return
        }
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// 调用时嵌套回调
fetchUser { result in
    switch result {
    case .success(let user):
        fetchPosts(for: user.id) { /* 更多嵌套... */ }
    case .failure(let error): 
        print(error)
    }
}

2. async/await 重构方案
(1) 定义异步网络请求
// 声明 async 函数,自动管理线程切换
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: userURL)
    return try JSONDecoder().decode(User.self, from: data)
}

func fetchPosts(userId: Int) async throws -> [Post] {
    let url = postsURL.appendingPathComponent("\(userId)")
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Post].self, from: data)
}

(2) 顺序调用(无嵌套)
Task {
    do {
        let user = try await fetchUser()
        let posts = try await fetchPosts(userId: user.id)
        updateUI(user: user, posts: posts)
    } catch {
        showError(error)
    }
}

(3) 并行请求优化

使用 async let 并行执行独立请求:

Task {
    async let user = fetchUser()      // 立即启动
    async let posts = fetchPosts()    // 立即启动
    
    do {
        // 同时等待两个结果
        let (finalUser, finalPosts) = try await (user, posts)
        updateUI(user: finalUser, posts: finalPosts)
    } catch {
        showError(error)
    }
}

3. 关键优势
  • 线性逻辑:代码顺序执行,消除回调嵌套
  • 错误集中处理:单点 try/catch 捕获所有异常
  • 性能提升
    • 零线程爆炸风险(系统自动管理线程池)
    • 挂起协程仅占用 2KB 内存(传统线程需 512KB)
  • 可取消性:通过 Task 句柄实现一键取消
    let task = Task {
        let data = try await heavyRequest()
        // ...
    }
    
    // 取消请求(自动传播取消信号)
    task.cancel()
    

4. 迁移建议
  1. 逐步替换 completionHandlerasync 函数
  2. 使用 @MainActor 标注 UI 更新方法:
    @MainActor
    func updateUI(user: User, posts: [Post]) {
        tableView.reloadData()  // 自动切换主线程
    }
    

  3. 兼容旧代码:用 withCheckedThrowingContinuation 包装回调
    func legacyRequest() async throws -> Data {
        return try await withCheckedThrowingContinuation { continuation in
            oldFetchMethod { result in
                continuation.resume(with: result)
            }
        }
    }
    

完整示例
struct API {
    static func loadFeed() async throws -> Feed {
        let (data, _) = try await URLSession.shared.data(
            from: URL(string: "https://api.example.com/feed")!
        )
        return try JSONDecoder().decode(Feed.self, from: data)
    }
}

// 使用端
Task {
    do {
        let feed = try await API.loadFeed()
        await MainActor.run { 
            self.feedView.update(with: feed) 
        }
    } catch {
        await MainActor.run { 
            self.showToast("加载失败: \(error.localizedDescription)")
        }
    }
}

注意:在 iOS 15+ 使用 URLSessionasync 方法,旧系统需通过 @available 做版本检查。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐