基于URLSession的多背景上传咨询:多类型资源上传与进度追踪
用Operations & OperationQueue实现多任务上传完全可行!
这个方向太合适了——Operation和OperationQueue本身就是为可取消、可追踪进度、可控制并发数的异步任务设计的,完美匹配你要实现的多文件/图片上传、每个页面独立任务池、实时追踪进度的需求。
一、先封装自定义上传Operation
你需要针对两种上传类型(UIImage和自定义结构体)封装对应的Operation子类,统一处理上传逻辑和进度回调:
1. 基础上传Operation模板
先写一个基类,封装通用的进度追踪和状态管理,避免重复代码:
import Foundation class BaseUploadOperation: Operation { // 进度回调,确保在主线程触发UI更新 var progressHandler: ((Double) -> Void)? // 完成回调,返回错误信息(如果有) var completionHandler: ((Error?) -> Void)? private(set) var progress: Double = 0.0 // 重写Operation的状态属性,保证KVO合规(Operation依赖这些状态做调度) private var isExecuting = false private var isFinished = false override var isExecuting: Bool { get { isExecuting } set { willChangeValue(forKey: "isExecuting") isExecuting = newValue didChangeValue(forKey: "isExecuting") } } override var isFinished: Bool { get { isFinished } set { willChangeValue(forKey: "isFinished") isFinished = newValue didChangeValue(forKey: "isFinished") } } override func start() { // 先检查任务是否已被取消,避免无效执行 if isCancelled { finish() return } isExecuting = true main() } override func main() { // 子类重写这个方法实现具体上传逻辑 } // 统一更新进度的方法,自动切回主线程 func updateProgress(_ value: Double) { progress = max(0.0, min(1.0, value)) DispatchQueue.main.async { self.progressHandler?(self.progress) } } // 统一结束任务的方法,更新状态并触发回调 func finish(with error: Error? = nil) { isExecuting = false isFinished = true DispatchQueue.main.async { self.completionHandler?(error) } } }
2. 针对UIImage的上传Operation
处理图片上传(先转成压缩后的Data,再发起上传):
class ImageUploadOperation: BaseUploadOperation { private let image: UIImage private let uploadURL: URL // 替换成你的实际上传接口地址 init(image: UIImage, uploadURL: URL) { self.image = image self.uploadURL = uploadURL super.init() } override func main() { guard !isCancelled else { finish() return } // 把UIImage转成JPEG Data(可根据需求调整压缩质量) guard let imageData = image.jpegData(compressionQuality: 0.8) else { finish(with: NSError(domain: "UploadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "图片转Data失败"])) return } // 构建上传请求 var request = URLRequest(url: uploadURL) request.httpMethod = "POST" request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type") // 创建URLSession上传任务 let task = URLSession.shared.uploadTask(with: request, from: imageData) { [weak self] data, response, error in guard let self = self, !self.isCancelled else { return } if let error = error { self.finish(with: error) return } // 检查服务器响应状态码 if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 { let error = NSError(domain: "UploadError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "上传失败,状态码:\(httpResponse.statusCode)"]) self.finish(with: error) } else { self.finish() } } // 监听上传进度 task.progress.addObserver(self, forKeyPath: "fractionCompleted", options: .new, context: nil) task.resume() } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "fractionCompleted", let progress = object as? Progress { updateProgress(progress.fractionCompleted) } } deinit { // 销毁时移除进度观察者,避免内存泄漏 URLSession.shared.getAllTasks { tasks in tasks.forEach { task in if task.state == .running { task.progress.removeObserver(self, forKeyPath: "fractionCompleted") } } } } }
3. 针对自定义结构体的上传Operation
处理带本地文件URL的自定义结构体(直接上传本地文件,无需转Data):
// 先定义你的自定义结构体 struct UploadItem { let fileURL: URL let metaData: [String: Any] // 附加的相关数据,比如文件名、分类等 } class FileUploadOperation: BaseUploadOperation { private let uploadItem: UploadItem private let uploadURL: URL init(item: UploadItem, uploadURL: URL) { self.uploadItem = item self.uploadURL = uploadURL super.init() } override func main() { guard !isCancelled else { finish() return } // 先检查本地文件是否存在 guard FileManager.default.fileExists(atPath: uploadItem.fileURL.path) else { finish(with: NSError(domain: "UploadError", code: -2, userInfo: [NSLocalizedDescriptionKey: "文件不存在"])) return } // 构建包含元数据的multipart请求(这里简化处理,实际需根据接口要求打包元数据) let boundary = "Boundary-\(UUID().uuidString)" var request = URLRequest(url: uploadURL) request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") // 创建基于本地文件的上传任务 let task = URLSession.shared.uploadTask(with: request, fromFile: uploadItem.fileURL) { [weak self] data, response, error in guard let self = self, !self.isCancelled else { return } if let error = error { self.finish(with: error) return } if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 { let error = NSError(domain: "UploadError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "上传失败,状态码:\(httpResponse.statusCode)"]) self.finish(with: error) } else { self.finish() } } // 监听上传进度 task.progress.addObserver(self, forKeyPath: "fractionCompleted", options: .new, context: nil) task.resume() } // 复用进度观察者逻辑 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "fractionCompleted", let progress = object as? Progress { updateProgress(progress.fractionCompleted) } } deinit { URLSession.shared.getAllTasks { tasks in tasks.forEach { task in if task.state == .running { task.progress.removeObserver(self, forKeyPath: "fractionCompleted") } } } } }
二、给每个ProgressViewController配置独立的任务池
每个VC维护自己的OperationQueue和上传任务数组,确保任务完全独立:
class ProgressViewController: UIViewController { // 独立的上传队列,控制最大并发数(比如同时上传3个) private let uploadQueue = OperationQueue() // 存储当前活跃的上传任务,方便追踪、取消单个任务 private var currentUploadOperations: [BaseUploadOperation] = [] override func viewDidLoad() { super.viewDidLoad() // 配置队列:设置并发数、命名方便调试 uploadQueue.maxConcurrentOperationCount = 3 uploadQueue.name = "com.yourapp.upload.queue.\(UUID().uuidString)" } // 对外暴露的上传方法,接收混合类型的数组 func startUploading(items: [Any]) { // 可选:先清空之前的未完成任务,根据业务需求调整 cancelAllUploads() for item in items { let operation: BaseUploadOperation // 区分上传类型,创建对应的Operation switch item { case let image as UIImage: operation = ImageUploadOperation(image: image, uploadURL: URL(string: "https://your-upload-api.com/image")!) case let uploadItem as UploadItem: operation = FileUploadOperation(item: uploadItem, uploadURL: URL(string: "https://your-upload-api.com/file")!) default: continue // 忽略不支持的类型 } // 绑定进度回调,更新对应任务的UI operation.progressHandler = { [weak self] progress in self?.updateTaskProgress(operation, progress: progress) } // 绑定完成回调,处理成功/失败逻辑 operation.completionHandler = { [weak self] error in if let error = error { print("任务上传失败:\(error.localizedDescription)") } else { print("任务上传成功") } // 从数组中移除已完成的任务 self?.currentUploadOperations.removeAll { $0 == operation } } // 添加到队列和任务数组 currentUploadOperations.append(operation) uploadQueue.addOperation(operation) } } // 更新单个任务的UI进度(根据你的UI布局实现) private func updateTaskProgress(_ operation: BaseUploadOperation, progress: Double) { // 比如可以给Operation加个自定义identifier,或者用数组索引匹配对应的进度条 // 示例:找到对应任务的UI组件,设置progress值 } // 取消所有上传任务 func cancelAllUploads() { uploadQueue.cancelAllOperations() currentUploadOperations.removeAll() } // 取消单个上传任务 func cancelSingleTask(_ operation: BaseUploadOperation) { operation.cancel() currentUploadOperations.removeAll { $0 == operation } } deinit { // 页面销毁时自动取消所有任务,避免内存泄漏 cancelAllUploads() } }
三、关键注意事项
- 线程安全:所有UI操作必须在主线程执行,上面的回调已经用
DispatchQueue.main.async处理,避免UI卡顿或崩溃。 - 任务取消:调用
operation.cancel()后,一定要在main()方法和回调中检查isCancelled属性,及时终止任务逻辑。 - 内存管理:用
weak self避免循环引用,尤其是在闭包和观察者中。 - 并发控制:通过
maxConcurrentOperationCount限制同时上传的任务数,避免服务器压力过大或本地网络拥堵。
这套实现完全能满足你“每个ProgressViewController独立上传数组+追踪每个任务进度”的需求,而且OperationQueue会自动处理任务调度、状态管理,比直接用URLSession任务更可控、更易维护。
内容的提问来源于stack exchange,提问作者Immanuel
相关产品推荐
相关产品推荐

