Use async/await with URLSession
Swift & UI 进阶 18m

在 URLSession 中使用 async/await

Use async/await with URLSession

2021年6月9日

在 Apple 官方观看视频

一句话判断

URLSession 的 async/await API 不是简单的语法糖 —— 它彻底消灭了嵌套回调地狱和手动管理 Resume Data 的复杂度,网络层代码量能砍掉一半,但你需要理解 structured concurrency 的取消语义才能用好它。

这场 Session 讲了什么

Session 讲解了 Swift 5.5 的 async/await 如何与 URLSession 集成。iOS 15 中 URLSession 新增了三个核心的 async 方法:data(for:)(普通请求)、data(for:delegate:)(带代理的请求)、upload(for:from:)(上传请求)。

这些方法直接返回 DataURLResponse,不再需要 closure 回调。错误处理用 try/catch,任务取消用 Task.cancel()。Session 用多个实例对比了 callback 版本和 async/await 版本的代码复杂度,展示了新 API 如何简化常见的网络编程模式:串行请求、并行请求、请求链、重试逻辑。

还讨论了 URLSessionTask 在 structured concurrency 中的生命周期管理。

值得深挖的点

data(for:delegate:) 的代理模式变化。在 callback 时代,如果你需要监听上传进度或处理重定向,需要设置 URLSessionTaskDelegate 并实现多个回调方法。async 版本通过 delegate 参数传入代理对象,请求完成后代理的回调方法仍然正常触发。但要注意:代理的回调方法在 data(for:delegate:) 的 async 调用返回之前执行,你不能在代理回调中再次 await 同一个 session 的方法,否则会死锁。

自动 Resume Data 处理。当网络请求因为取消或失败需要重试时,传统 API 需要手动检查 error.userInfo[NSURLSessionDownloadTaskResumeData] 来保存断点。async/await 版本通过 URLError.downloadResumeData 直接在 catch 中暴露 resume data,配合 Task 的取消机制,重试逻辑变得非常干净。

代码片段

基础 async/await 网络请求

import Foundation

// 对比:callback 版本 vs async/await 版本

// === 旧写法:嵌套回调 ===
func fetchUserCallback(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(NSError(domain: "NoData", code: -1)))
            return
        }
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// === 新写法:async/await ===
func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(User.self, from: data)
}

并行请求和取消

import Foundation

func loadDashboard() async throws -> Dashboard {
    // 三个请求并行发起
    async let userProfile = fetchUser(id: currentUserID)
    async let feedItems = fetchFeed(page: 1)
    async let notifications = fetchNotifications()

    // 等待所有请求完成
    return try await Dashboard(
        user: userProfile,
        feed: feedItems,
        notifications: notifications
    )
}

// 带超时和取消的网络请求
func fetchWithTimeout(url: URL, timeout: TimeInterval = 10) async throws -> Data {
    try await withThrowingTaskGroup(of: Data.self) { group in
        group.addTask {
            let (data, _) = try await URLSession.shared.data(from: url)
            return data
        }
        group.addTask {
            try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
            throw URLError(.timedOut)
        }
        // 返回最先完成的结果
        let result = try await group.next()!
        group.cancelAll()  // 取消超时任务
        return result
    }
}

带代理的请求(监听进度)

import Foundation

class DownloadManager: NSObject, URLSessionTaskDelegate {
    let session: URLSession

    override init() {
        self.session = URLSession(configuration: .default, delegate: nil, delegateQueue: nil)
        super.init()
        self.session = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
    }

    func downloadFile(from url: URL) async throws -> URL {
        // 使用带代理的 async 方法
        let (tempURL, response) = try await session.download(for: URLRequest(url: url), delegate: self)
        return tempURL
    }

    // 代理回调:监听下载进度
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        if let error = error {
            print("下载失败: \(error)")
        }
    }
}

最佳实践

  1. 把网络层统一封装成 async 函数。不要在部分地方用 async/await、部分地方用 callback。统一之后可以消除所有的 completion handler 模板代码。
  2. 并行请求用 async let 而不是 TaskGroup,除非你需要动态数量的并发任务。async let 更直观,编译器会帮你管理生命周期。
  3. 请求取消要处理 partial data。当 Task.cancel() 被调用时,正在进行的网络请求不会立即中断,而是在下一次 suspend point 抛出 CancellationError。如果你需要保存中间结果,在 catch 块中处理。

还有什么值得关注

  • URLSession 的 async API 同时支持 datadownloadupload 三种任务类型。
  • try await URLSession.shared.data(from:) 自动处理 HTTP 重定向(最多 5 次)。
  • 在 Swift concurrency 中,URLSession 的 delegate queue 应设为 nil(使用 cooperative thread pool)而不是手动指定 OperationQueue。
WWDC 2021