如何在Alamofire重试请求后追踪URLSessionTask并匹配数据库实体
我之前在做Alamofire 4的后台上传功能时,也碰到过一模一样的问题——重试时taskDescription和taskIdentifier全失效,根本没法关联数据库里的实体。结合你要实现上传成功后删文件和实体的需求(哪怕应用终止后也要生效),这里有几个我亲测好用的方案:
1. 用Request的userInfo字典传递UUID
Alamofire 4的Request自带userInfo可变字典,专门用来存自定义信息。核心是在重试时手动把UUID从原请求复制到新请求里:
- 第一步:创建上传请求时,把数据库实体的UUID塞到
userInfo里:
let uploadRequest = Alamofire.upload(multipartFormData: { formData in // 添加上传内容 }, to: uploadUrl) uploadRequest.userInfo["entityUUID"] = yourEntityUUIDString
- 第二步:自定义重试处理器,把原请求的UUID同步给新请求:
class UploadRetryHandler: RequestRetrier { func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { // 这里判断是否需要重试,比如网络超时、5xx错误 guard shouldRetry(for: error) else { completion(false, 0) return } // 把原请求的UUID复制到重试请求的userInfo里 request.userInfo.forEach { key, value in request.retryRequest?.userInfo[key] = value } completion(true, 1.5) // 1.5秒后重试 } private func shouldRetry(for error: Error) -> Bool { // 自定义重试逻辑 let nsError = error as NSError return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut } }
- 第三步:响应回调里从
userInfo取UUID,处理删除:
uploadRequest.responseJSON { [weak self] response in guard let self = self, response.error == nil else { return } if let uuid = response.request?.userInfo["entityUUID"] as? String { self.deleteUploadedEntityAndFile(uuid: uuid) } }
优点:完全用Alamofire原生属性,逻辑简单直接。
注意:应用重启后内存里的userInfo会丢失,所以必须配合数据库存上传状态,重启后检查已完成的任务再处理删除。
2. 自定义HTTP头带UUID
把UUID放进请求的自定义HTTP头里,不管Alamofire怎么重试,请求头都会被保留下来:
- 第一步:创建请求时加自定义头:
let customHeaders: HTTPHeaders = [ "X-Upload-Entity-ID": yourEntityUUIDString ] let uploadRequest = Alamofire.upload(multipartFormData: { ... }, to: uploadUrl, headers: customHeaders)
- 第二步:响应回调里从请求头解析UUID:
uploadRequest.responseJSON { [weak self] response in guard let self = self, response.error == nil else { return } if let uuid = response.request?.allHTTPHeaderFields?["X-Upload-Entity-ID"] { self.deleteUploadedEntityAndFile(uuid: uuid) } }
优点:完全不依赖内存状态,哪怕应用重启后后台任务完成,只要请求没被修改,就能从请求头拿到UUID,特别适合后台上传场景。
注意:要确认服务器不会拦截这个自定义头,如果担心的话,可以让服务器在响应头里把这个UUID返回回来,双重验证关联关系。
3. 把UUID嵌进文件名里
直接把数据库实体的UUID作为上传文件的一部分(比如前缀),这样任务完成后,通过文件路径就能反向解析出UUID:
- 第一步:上传前重命名文件,把UUID嵌进去:
let originalFileUrl = URL(fileURLWithPath: "/path/to/your/file.jpg") let uuid = yourEntityUUIDString let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) let newFileUrl = tempDir.appendingPathComponent("\(uuid)_upload_file.jpg") // 复制原文件到临时目录(避免原文件被误删) do { try FileManager.default.copyItem(at: originalFileUrl, to: newFileUrl) } catch { // 处理复制错误 }
- 第二步:用新文件路径创建上传请求,同时把新路径存到数据库关联实体:
let uploadRequest = Alamofire.upload(fileURL: newFileUrl, to: uploadUrl) // 数据库里记录:实体UUID ↔ 上传文件路径
- 第三步:任务完成后解析文件名拿UUID,删除文件和实体:
uploadRequest.responseJSON { [weak self] response in guard let self = self, response.error == nil else { return } if let fileUrl = response.request?.url { let fileName = fileUrl.lastPathComponent let uuid = fileName.components(separatedBy: "_").first ?? "" self.deleteUploadedEntityAndFile(uuid: uuid, filePath: fileUrl.path) } }
优点:完全脱离Alamofire的任务属性,哪怕任务被重建、应用重启,只要文件存在就能关联到实体,可靠性拉满。
注意:要处理文件重命名的异常情况,比如文件名解析失败的兜底逻辑。
4. 持久化任务映射表+SessionDelegate追踪
建一个数据库表存taskIdentifier和UUID的关联,再通过SessionDelegate追踪任务的生命周期,更新映射关系:
第一步:数据库建表,字段包括
task_id(整数)、entity_uuid(字符串)、status(上传中/已完成/失败)。第二步:创建请求时,把初始
taskIdentifier和UUID存进数据库:
let uploadRequest = Alamofire.upload(...) if let taskId = uploadRequest.task?.taskIdentifier { // 插入数据库记录 UploadTaskRecord.create(taskId: taskId, entityUUID: yourEntityUUIDString, status: .uploading) }
- 第三步:自定义SessionDelegate,在重试生成新任务时更新映射:
class CustomSessionDelegate: SessionDelegate { override func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { // 如果是重试生成的新任务,找到原任务的UUID,关联新的taskIdentifier if let originalTask = task.originalTask, let originalId = originalTask.taskIdentifier { if let record = UploadTaskRecord.find(byTaskId: originalId) { UploadTaskRecord.create(taskId: task.taskIdentifier, entityUUID: record.entityUUID, status: .uploading) // 删除原任务的映射记录 record.delete() } } super.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) } }
- 第四步:任务完成时,通过
taskIdentifier拿UUID处理删除:
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard error == nil else { return } if let record = UploadTaskRecord.find(byTaskId: task.taskIdentifier) { deleteUploadedEntityAndFile(uuid: record.entityUUID) record.delete() } }
优点:能精准追踪每个任务的关联关系,适合复杂的上传任务管理场景。
注意:iOS重启后taskIdentifier会重置,所以应用启动时要清理数据库里无效的映射记录。
最终推荐
结合你后台上传+应用终止后仍需处理的需求,优先选方案2(自定义HTTP头)或方案3(UUID嵌文件名),这俩方案的关联信息不依赖内存,哪怕应用重启后后台任务跑完,也能稳稳关联到数据库实体,执行删除操作。
内容的提问来源于stack exchange,提问作者uson1x

