WWDC Quick Look 💓 By SwiftGGTeam
Use async/await with URLSession

Use async/await with URLSession

观看原视频

Highlight

iOS 15 为 URLSession 新增了 data(for:)upload(for:from:)download(from:)bytes(from:) 等原生 async 方法,支持通过 task-specific delegate 处理认证挑战,同时引入 AsyncSequence 用于增量接收响应体。

核心内容

为什么要引入 async/await 版本的 URLSession?

一个常见的网络请求代码,使用 completion handler 存在三个问题:

  1. 控制流跳来跳去:创建 task → resume → 跳到 completionHandler → 跳到主队列的 completionHandler
  2. 线程安全陷阱:涉及三个执行上下文(调用者队列、session delegate 队列、主队列),编译器无法帮助检测数据竞争
  3. 错误处理困难:completionHandler 可能被调用两次,UIImage(data:) 返回 optional 但没有对应的 error

改成 async/await 后,控制流从上到下线性执行,所有代码在同一并发上下文中运行,不需要担心线程问题,错误用 throw 处理,编译器会强制处理 optional。

URLSession 的 async 方法

iOS 15 和 macOS Monterey 新增的 async 方法包括:

  • data(from:) / data(for:):获取数据,等效于 data task convenience 方法
  • upload(for:from:):上传数据或文件,等效于 upload task
  • download(from:) / download(for:):下载文件到磁盘,不自动删除,需要手动移动或删除
  • bytes(from:) / bytes(for:):返回 AsyncBytes,用于增量接收响应体

所有方法都支持 Swift 原生的 try-catch 错误处理和 Task 取消。

处理增量响应:URLSession.bytes

datauploaddownload 方法都等待完整响应体到达后才返回。如果需要增量处理(如 SSE、流式 JSON),使用 bytes 方法。

bytes(from:) 在响应头到达后立即返回,响应体以 AsyncSequence<UInt8> 形式增量交付。AsyncBytes 提供 lines 转换,按行解析响应体。

Task-specific Delegate

URLSession 的 delegate 模型提供了认证挑战、metrics 等回调。新的 async 方法不再暴露底层 task,如何处理特定任务的认证挑战?

答案是为 async 方法传入一个 task-specific delegate 参数。这个 delegate 只处理该特定 task 的事件,且被 task 强引用直到完成或失败。

如果 session delegate 和 task delegate 都实现了同一个方法,task delegate 的实现优先。注意:Background URL Session 不支持 task-specific delegate。

Swift Concurrency 取消机制

Task 类型有 cancel() 方法,但注意 Swift Concurrency 的 TaskURLSessionTask 是完全不同的概念,只是名字相似。

取消是协作式的——调用 cancel() 后,代码在下一个 suspend point(如 await)检查取消状态并抛出 CancellationError

详细内容

获取照片的 async 版本

02:52

func fetchPhoto(url: URL) async throws -> UIImage
{
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw WoofError.invalidServerResponse
    }

    guard let image = UIImage(data: data) else {
        throw WoofError.unsupportedImage
    }

    return image
}

关键点:

  • data(from:) 返回 (Data, URLResponse) 元组
  • guard 检查状态码,不符合则 throw 错误
  • UIImage(data:) 返回 optional,必须处理 nil 情况
  • 整个函数是 async throws,调用时用 try await

URL Resquest 版本的 data 方法

03:45

let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw MyNetworkingError.invalidServerResponse
}

关键点:

  • data(for:) 接受 URLRequest 参数,可以配置 HTTP 头、方法等
  • 响应检查模式相同:guard cast + 检查状态码

上传文件

04:03

var request = URLRequest(url: url)
request.httpMethod = "POST"

let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 201 else {
    throw MyNetworkingError.invalidServerResponse
}

关键点:

  • upload(for:fromFile:) 从磁盘文件上传,适合大文件
  • 上传成功通常返回 201 Created,检查对应的状态码
  • 也有 upload(for:from:) 版本,从 Data 上传

下载文件

04:21

let (location, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw MyNetworkingError.invalidServerResponse
}

try FileManager.default.moveItem(at: location, to: newLocation)

关键点:

  • download(from:) 返回临时文件 URL,需要手动移动到目标位置
  • 旧版 convenience 方法会自动删除文件,新版不会
  • 务必在处理完成后删除或移动临时文件

任务取消

04:44

let task = Task {
    let (data1, response1) = try await URLSession.shared.data(from: url1)
    let (data2, response2) = try await URLSession.shared.data(from: url2)
}

task.cancel()

关键点:

  • Task { } 创建 structured concurrency 任务,返回 Task.Handle
  • cancel() 在下一个 await 点生效,抛出 CancellationError
  • 如果第一个 await 已经返回,取消不会影响第二个请求,两个串行请求都会完成

增量接收响应体

07:53

let (bytes, response) = try await URLSession.shared.bytes(from: Self.eventStreamURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
    throw WoofError.invalidServerResponse
}
for try await line in bytes.lines {
    let photoMetadata = try JSONDecoder().decode(PhotoMetadata.self, from: Data(line.utf8))
    await updateFavoriteCount(with: photoMetadata)
}

关键点:

  • bytes(from:) 返回 (URLSession.AsyncBytes, URLResponse) 元组
  • AsyncBytes.lines\n 分割响应体,返回 AsyncSequence<String>
  • for try await 循环中处理每一行,适合 SSE、流式 JSON 场景
  • await updateFavoriteCount 调用另一个 async 函数更新 UI

Task-specific Delegate 处理认证

11:20

class AuthenticationDelegate: NSObject, URLSessionTaskDelegate {
    private let signInController: SignInController

    init(signInController: SignInController) {
        self.signInController = signInController
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
            do {
                let (username, password) = try await signInController.promptForCredential()
                return (.useCredential,
                        URLCredential(user: username, password: password, persistence: .forSession))
            } catch {
                return (.cancelAuthenticationChallenge, nil)
            }
        } else {
            return (.performDefaultHandling, nil)
        }
    }
}

使用方式:

let delegate = AuthenticationDelegate(signInController: signInController)
let (data, response) = try await URLSession.shared.data(for: request, delegate: delegate)

关键点:

  • delegate 方法本身可以是 async,支持 await 调用
  • delegate 被 task 强引用,不需要手动持有
  • 对于 HTTP Basic 认证,可以弹出登录框获取凭据
  • 返回 (useCredential, credential) 告诉 session 使用凭据继续请求

核心启发

  1. 将现有网络层迁移到 async/await

    • 做什么:把所有基于 completion handler 的网络请求方法改成 async throws 版本
    • 为什么值得做:代码可读性大幅提升,错误处理更安全,减少线程安全 bug
    • 怎么开始:从最常用的请求方法开始,把 completionHandler: @escaping (Result<T, Error>) -> Void 改成 async throws -> T。入口 API 是 URLSession.data(for:)
  2. 为实时更新接口接入 bytes API

    • 做什么:用 URLSession.bytes(from:) 替代轮询或 WebSocket 处理 SSE/流式接口
    • 为什么值得做:增量接收响应体,内存占用更低,延迟更小
    • 怎么开始:检查现有 SSE 接口,用 bytes.lines 按行解析,在 for try await 循环中处理每条消息
  3. 用 task-specific delegate 简化认证逻辑

    • 做什么:为需要特殊认证的请求创建专用 delegate 类,而不是在全局 delegate 中用 task ID 分发
    • 为什么值得做:delegate 逻辑与请求逻辑放在一起,更易维护
    • 怎么开始:实现 URLSessionTaskDelegate,在 data(for:delegate:) 中传入实例。delegate 方法可以标记为 async
  4. 优化文件下载的内存占用

    • 做什么:对大文件下载使用 download(from:) 而不是 data(from:)
    • 为什么值得做data 将整个响应体加载到内存,download 写入临时文件,内存占用恒定
    • 怎么开始:将所有图片、视频下载改为 download,下载完成后用 FileManager.moveItem 移动到目标位置

关联 Session

评论

GitHub Issues · utterances