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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 04:11:11