You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

URLSession UploadTask代理触发时,如何处理ResumeData实现中断续传?

上传任务断点续传实现方案

首先明确:URLSessionUploadTask本身并不像下载任务那样提供现成的resumeData续传API,上传续传的核心依赖服务端支持断点续传协议(比如HTTP的Range/Content-Range头,或者自定义进度记录接口)。以下是具体落地步骤:

1. 前置准备:确认服务端支持断点续传

服务端需要具备以下能力:

  • 接收PUT请求时,能识别Range头,确认已接收的文件字节范围
  • 可返回已接收的字节数(比如响应头返回Content-Range,或通过单独接口查询)
  • 支持将后续上传的字节追加到已接收的文件片段上

2. 客户端实时记录上传进度

在上传过程中,通过代理方法实时记录已上传字节数并持久化:

func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
    // 用任务ID关联进度,存储到UserDefaults/Core Data等持久化容器
    UserDefaults.standard.set(totalBytesSent, forKey: "uploadProgress_\(task.taskIdentifier)")
}

3. 中断场景下的续传逻辑处理

当上传因网络中断、APP被后台杀死等场景失败时,在代理错误回调中处理续传:

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    guard let error = error else { return }
    
    let nsError = error as NSError
    // 只处理可恢复的错误类型
    let recoverableCodes = [NSURLErrorNetworkConnectionLost, NSURLErrorBackgroundSessionWasDisconnected]
    guard recoverableCodes.contains(nsError.code) else {
        os_log("不可恢复的上传错误:%@", type: .error, error.localizedDescription)
        return
    }
    
    // 获取必要的续传信息
    guard let uploadTask = task as? URLSessionUploadTask,
          let fileURL = uploadTask.originalRequest?.httpBodyFileURL,
          let totalBytesSent = UserDefaults.standard.object(forKey: "uploadProgress_\(task.taskIdentifier)") as? Int64,
          let totalBytesExpected = task.countOfBytesExpectedToSend,
          totalBytesSent < totalBytesExpected else {
        // 无法获取有效信息,重新上传整个文件
        restartFullUpload(fileURL: fileURL)
        return
    }
    
    // 构造续传请求:告知服务端从已上传字节位置开始接收
    var resumeRequest = uploadTask.originalRequest!
    resumeRequest.setValue("bytes \(totalBytesSent)-", forHTTPHeaderField: "Range")
    // 部分服务端要求用Content-Range头,需和服务端确认格式
    // resumeRequest.setValue("bytes \(totalBytesSent)-\(totalBytesExpected-1)/\(totalBytesExpected)", forHTTPHeaderField: "Content-Range")
    
    // 读取文件未上传的部分,创建新的上传任务
    do {
        let fileHandle = try FileHandle(forReadingFrom: fileURL)
        fileHandle.seek(toFileOffset: UInt64(totalBytesSent))
        let remainingData = fileHandle.readDataToEndOfFile()
        fileHandle.closeFile()
        
        let resumeTask = urlSession.uploadTask(with: resumeRequest, from: remainingData)
        resumeTask.resume()
        // 清除旧任务的进度记录,关联新任务
        UserDefaults.standard.removeObject(forKey: "uploadProgress_\(task.taskIdentifier)")
    } catch {
        // 文件读取失败,重新上传
        restartFullUpload(fileURL: fileURL)
    }
}

// 重新上传整个文件的辅助方法
func restartFullUpload(fileURL: URL) {
    guard let uploadUrl = URL(string: "你的上传接口地址") else { return }
    var request = URLRequest(url: uploadUrl)
    request.httpMethod = "PUT"
    let uploadTask = urlSession.uploadTask(with: request, fromFile: fileURL)
    uploadTask.resume()
}

4. 后台Session的额外处理

因为你使用的是后台URLSession,需注意两点:

  • APP被从多任务栏移除时,在applicationWillTerminate(_:)中批量持久化所有活跃上传任务的进度
  • APP重启后,调用urlSession.getTasksWithCompletionHandler(_:)获取未完成的后台任务,结合之前存储的进度处理续传

关键注意事项

  • 必须和服务端确认断点续传的具体协议细节(头字段格式、是否需要文件哈希验证等)
  • 持久化进度时,建议关联文件唯一标识(如MD5),避免任务ID冲突导致进度混乱
  • 处理超大文件时,直接读取剩余数据到内存会引发内存压力,建议使用FileHandle分块上传或URLSessionStreamTask(实现复杂度更高)

内容的提问来源于stack exchange,提问作者Mumthezir VP

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 17:40:25