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
相关产品推荐
相关产品推荐

