WWDC Quick Look 💓 By SwiftGGTeam
Build robust and resumable file transfers

Build robust and resumable file transfers

观看原视频

Highlight

URLSession 在 iOS 17 中为上传任务(Upload Task)新增了与下载任务(Download Task)一致的暂停/恢复 API,并引入了基于 IETF 标准的可恢复上传协议,同时新增了后台传输的精细化控制能力。


核心内容

开发一个需要传输大文件的应用时,网络中断是常态而非意外。用户进电梯、切换 Wi-Fi、走到信号盲区,正在进行的几 GB 下载或上传就可能中断,需要从头再来。iOS 17 之前的 URLSession 对下载任务有 cancelByProducingResumeData 这套暂停恢复机制,但上传任务没有对等的 API。如果你想做一个能断点续传的文件上传功能,只能自己在应用层实现分片逻辑。

iOS 17 给上传任务补齐了这块短板。URLSessionUploadTask 现在支持 cancelByProducingResumeData() 暂停上传并生成恢复数据,也支持 uploadTask(withResumeData:) 从恢复点继续。API 形状与下载任务完全一致,学习成本为零。

但光有客户端 API 还不够。暂停恢复机制需要服务器配合——服务器必须理解客户端从哪里继续上传。iOS 17 引入了基于最新 IETF 标准草案的可恢复上传协议(Resumable Uploads Protocol)。Apple 还发布了 SwiftNIO 的服务端参考实现,方便后端团队快速对接。

此外,Background URLSession 在 iOS 17 中获得了更细粒度的控制:可以设置任务的最早开始时间、预估传输字节数以优化系统调度,以及声明是否允许使用受限网络。


详细内容

下载任务的暂停与恢复(04:53

URLSession 的下载任务暂停恢复机制基于 HTTP Range 请求。服务器通过 Accept-Ranges 响应头声明支持范围请求,通过 ETag 标识资源版本。中断后客户端发送 Range 请求并附带 If-Range 头,确保资源未被修改。

创建下载任务后,用 cancelByProducingResumeData() 替代普通的 cancel()

let downloadTask = session.downloadTask(with: request)
downloadTask.resume()

guard let resumeData = await downloadTask.cancelByProducingResumeData() else {
    // Download cannot be resumed
    return
}

恢复数据(resume data)是一个二进制 blob,包含了已下载的字节、原始请求和服务器验证信息。用它创建新的下载任务即可从断点继续:

let downloadTask = session.downloadTask(with: request)
downloadTask.resume()

guard let resumeData = await downloadTask.cancelByProducingResumeData() else {
    // Download cannot be resumed
    return
}

let newDownloadTask = session.downloadTask(withResumeData: resumeData)
newDownloadTask.resume()

关键点:

  • cancelByProducingResumeData() 是异步方法,会等待系统生成恢复数据后再返回
  • 恢复数据不可序列化到本地——它可能包含临时文件路径,应用重启后失效
  • 返回 nil 表示服务器不支持断点续传,此时只能从头下载

如果下载因网络错误而意外中断,可以从 URLError 中提取恢复数据(06:34):

do {
    let (url, response) = try await session.download(for: request)
} catch let error as URLError {
    guard let resumeData = error.downloadTaskResumeData else {
        // Download cannot be resumed
        return
    }
}

关键点:

  • URLError.downloadTaskResumeData 只在下载任务的错误中包含
  • 处理错误时先检查恢复数据是否存在,再决定是否从头重试

上传任务的暂停与恢复(08:29

iOS 17 之前,上传任务只能用 cancel() 彻底取消,没有任何恢复机制。新 API 与下载任务完全对称。

暂停上传:

let uploadTask = session.uploadTask(with: request, fromFile: fileURL)
uploadTask.resume()

guard let resumeData = await uploadTask.cancelByProducingResumeData() else {
    // Upload cannot be resumed
    return
}

恢复上传:

let uploadTask = session.uploadTask(with: request, fromFile: fileURL)
uploadTask.resume()

guard let resumeData = await uploadTask.cancelByProducingResumeData() else {
    // Upload cannot be resumed
    return
}

let newUploadTask = session.uploadTask(withResumeData: resumeData)
newUploadTask.resume()

从错误中恢复上传(09:22):

do {
    let (data, response) = try await session.upload(for: request, fromFile: fileURL)
} catch let error as URLError {
    guard let resumeData = error.uploadTaskResumeData else {
        // Upload cannot be resumed
        return
    }
}

关键点:

  • 上传恢复数据与下载恢复数据是不同类型,不能混用
  • 如果只是短暂中断且服务器可达,URLSession 会自动尝试恢复上传,无需手动处理
  • 上传恢复需要服务器支持最新的 IETF 可恢复上传协议草案

可恢复上传协议与 SwiftNIO 服务端实现(13:15

客户端暂停上传后,恢复时向服务器发送一个特殊的 HTTP 请求,询问”我已经上传了多少字节,从哪里继续”。服务器根据之前接收的数据返回偏移量,客户端从该偏移量继续发送。

这套协议需要服务器端配合实现。Apple 在 SwiftNIO 中提供了开箱即用的 NIOResumableUpload 模块。

没有可恢复上传支持的 SwiftNIO 服务端代码:

NIOTSListenerBootstrap(group: NIOTSEventLoopGroup())
    .childChannelInitializer { channel in
        channel.configureHTTP2Pipeline(mode: .server) { channel in
            channel.pipeline.addHandlers([
                HTTP2FramePayloadToHTTPServerCodec(),
                ExampleChannelHandler()
            ])
        }.map { _ in () }
    }
    .tlsOptions(tlsOptions)

添加可恢复上传支持后(14:06):

import NIOResumableUpload

let uploadContext = HTTPResumableUploadContext(origin: "https://example.com")

NIOTSListenerBootstrap(group: NIOTSEventLoopGroup())
    .childChannelInitializer { channel in
        channel.configureHTTP2Pipeline(mode: .server) { channel in
            channel.pipeline.addHandlers([
                HTTP2FramePayloadToHTTPServerCodec(),
                HTTPResumableUploadHandler(context: uploadContext, handlers: [
                    ExampleChannelHandler()
                ])
            ])
        }.map { _ in () }
    }
    .tlsOptions(tlsOptions)

关键点:

  • HTTPResumableUploadContextorigin 参数声明允许恢复上传的来源域名
  • HTTPResumableUploadHandler 包裹在原有的业务 handler 外层,自动处理协议握手
  • 该模块基于 HTTP/2,需要先配置 HTTP/2 pipeline
  • NIOResumableUpload 是独立的 Swift package,需要单独引入

信息响应代理方法(15:48

可恢复上传协议依赖 HTTP 信息响应(1xx 状态码)来协商恢复参数。iOS 17 新增了 URLSessionTaskDelegate 的方法来接收这些响应:

protocol URLSessionTaskDelegate : URLSessionDelegate {
    optional func urlSession(_ session: URLSession, task: URLSessionTask,
                             didReceiveInformationalResponse response: HTTPURLResponse)
}

关键点:

  • 信息响应(如 100 Continue104 Upload Resumption)不会被视作最终响应
  • 这个方法在收到每个信息响应时调用一次
  • 对于可恢复上传,系统内部使用此机制处理协议协商,通常不需要手动干预

Background URLSession 最佳实践(18:19

Background URLSession 让传输任务在应用被挂起后仍能继续运行。iOS 17 增加了更精细的控制选项:

// Configuring your background session
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app")
configuration.isDiscretionary = true
configuration.allowsConstrainedNetworkAccess = false
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

// Configuring your background task
let backgroundTask = session.uploadTask(with: url, fromFile: fileURL)
backgroundTask.earliestBeginDate = .now.addingTimeInterval(60 * 60)
backgroundTask.countOfBytesClientExpectsToSend = 500 * 1024
backgroundTask.countOfBytesClientExpectsToReceive = 200

关键点:

  • isDiscretionary = true:系统在 Wi-Fi 且电量充足时才执行传输,适合非紧急内容
  • allowsConstrainedNetworkAccess = false:禁止使用低数据模式或热点等受限网络
  • earliestBeginDate:设置任务的最早开始时间,例如推迟到凌晨低峰期执行
  • countOfBytesClientExpectsToSend/Receive:预估传输字节数,帮助系统优化调度和电量消耗
  • Background URLSession 的 delegate 必须设置,否则无法接收完成回调

核心启发

1. 做一个带进度条的断点续传下载管理器

URLSession 的下载暂停恢复 API 已经足够成熟,结合 URLSessionDownloadDelegate 的进度回调,可以封装一个 ResumableDownloadManager,管理多个下载任务的状态(下载中/已暂停/已完成)、进度和恢复数据。用户手动暂停时调用 cancelByProducingResumeData(),将恢复数据存到本地(注意处理临时文件路径失效问题),下次启动时重建任务。

2. 为社交应用添加大视频断点上传

利用新的上传恢复 API,做一个类似微信朋友圈的视频上传体验:用户选好视频后立即开始上传,退出应用或切到后台时自动暂停并保存恢复数据,下次打开自动继续。关键点是恢复数据的生命周期管理——需要将恢复数据序列化到磁盘,并在恢复后清理。

3. 为后端团队提供 SwiftNIO 可恢复上传接入方案

如果团队使用 Swift 后端(Vapor 或裸 SwiftNIO),可以直接引用 NIOResumableUpload 模块,按照 session 中的代码模板接入。关键配置是 HTTPResumableUploadContextorigin 参数和 pipeline handler 的顺序。客户端无需额外代码——URLSession 的上传恢复 API 自动使用标准协议。

4. 后台内容预加载系统

结合 Background URLSession 的 earliestBeginDateisDiscretionary 配置,做一个新闻或视频应用的离线预加载功能。在应用前台时,为每个待预加载的资源创建后台任务,设置凌晨时段执行。系统会在 Wi-Fi + 充电条件下自动完成下载,用户早晨打开应用时内容已就绪。


关联 Session

评论

GitHub Issues · utterances