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

如何基于URLSessionDownloadTask高效追踪多文件合并下载进度

优化多文件下载整体进度追踪的高效方案

你之前的方案每次下载回调都要遍历所有任务筛选并求和,属于O(n)操作,回调频繁时必然导致高CPU负载。核心优化思路是提前分组缓存关键数据,每次回调只做增量更新,将操作复杂度降到O(1)。以下提供两种适配不同需求的方案:

方案一:基于任务平均进度的追踪(匹配你原需求)

保持你原有的平均进度逻辑,通过缓存避免重复计算:

代码示例

class DownloadTaskHandler: NSObject, URLSessionDownloadDelegate, ObservableObject {
    @Published var activeTasks: [URL:Download]
    // 对外暴露的各MyObject整体进度
    @Published var objectOverallProgress: [MyObject.ID: Float] = [:]
    
    // 内部缓存:key为MyObject.id
    private var objectTaskCount: [MyObject.ID: Int] = [:]
    private var objectCumulativeProgress: [MyObject.ID: Float] = [:]
    
    init(activeTasks: [URL:Download] = [:]) {
        self.activeTasks = activeTasks
        super.init()
        // 初始化已有任务的缓存
        for download in activeTasks.values {
            let id = download.object.id
            objectTaskCount[id, default: 0] += 1
            objectCumulativeProgress[id, default: 0] += download.progress
        }
    }
    
    // 添加新下载任务时调用
    func addDownload(_ download: Download) {
        guard let url = download.task?.originalRequest?.url else { return }
        activeTasks[url] = download
        let id = download.object.id
        objectTaskCount[id, default: 0] += 1
        objectCumulativeProgress[id, default: 0] += download.progress
        updateOverallProgress(for: id)
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        guard let url = downloadTask.originalRequest?.url,
              var download = activeTasks[url] else { return }
        
        let newProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        // 计算进度变化量
        let progressDelta = newProgress - download.progress
        download.progress = newProgress
        activeTasks[url] = download
        
        // 增量更新累计进度
        let objectId = download.object.id
        objectCumulativeProgress[objectId, default: 0] += progressDelta
        updateOverallProgress(for: objectId)
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        guard let url = downloadTask.originalRequest?.url,
              var download = activeTasks[url] else { return }
        
        // 标记任务完成,进度设为1.0
        let progressDelta = 1.0 - download.progress
        download.progress = 1.0
        download.isActive = false
        activeTasks[url] = download
        
        let objectId = download.object.id
        objectCumulativeProgress[objectId, default: 0] += progressDelta
        updateOverallProgress(for: objectId)
        
        // 后续文件移动等逻辑...
        // 若移除任务,同步更新缓存:
        // objectTaskCount[objectId]? -= 1
        // activeTasks.removeValue(forKey: url)
    }
    
    // 私有方法:更新单个MyObject的整体进度到主线程
    private func updateOverallProgress(for objectId: MyObject.ID) {
        guard let count = objectTaskCount[objectId], count > 0,
              let cumulative = objectCumulativeProgress[objectId] else { return }
        DispatchQueue.main.async {
            self.objectOverallProgress[objectId] = cumulative / Float(count)
        }
    }
}

方案二:基于总字节数的进度追踪(更符合实际下载体验)

平均进度会忽略文件大小差异,大文件和小文件权重相同,实际体验不够准确。推荐使用总已下载字节数/总期望字节数的方式计算整体进度:

代码示例

class DownloadTaskHandler: NSObject, URLSessionDownloadDelegate, ObservableObject {
    @Published var activeTasks: [URL:Download]
    @Published var objectOverallProgress: [MyObject.ID: Float] = [:]
    
    // 内部缓存:key为MyObject.id
    private var objectTotalBytes: [MyObject.ID: Int64] = [:]
    private var objectCompletedBytes: [MyObject.ID: Int64] = [:]
    // 记录单个任务上次的已下载字节数,用于计算增量
    private var taskLastWrittenBytes: [URL: Int64] = [:]
    
    init(activeTasks: [URL:Download] = [:]) {
        self.activeTasks = activeTasks
        super.init()
        // 初始化已有任务的缓存
        for download in activeTasks.values {
            guard let task = download.task else { continue }
            let id = download.object.id
            objectTotalBytes[id, default: 0] += task.countOfBytesExpectedToReceive
            let completed = task.countOfBytesReceived
            objectCompletedBytes[id, default: 0] += completed
            taskLastWrittenBytes[download.task!.originalRequest!.url!] = completed
        }
        // 初始化所有对象的整体进度
        for id in objectTotalBytes.keys {
            updateOverallProgress(for: id)
        }
    }
    
    func addDownload(_ download: Download) {
        guard let url = download.task?.originalRequest?.url,
              let task = download.task else { return }
        activeTasks[url] = download
        let id = download.object.id
        objectTotalBytes[id, default: 0] += task.countOfBytesExpectedToReceive
        let completed = task.countOfBytesReceived
        objectCompletedBytes[id, default: 0] += completed
        taskLastWrittenBytes[url] = completed
        updateOverallProgress(for: id)
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        guard let url = downloadTask.originalRequest?.url,
              let download = activeTasks[url] else { return }
        
        let objectId = download.object.id
        // 计算本次回调新增的已下载字节数
        let lastBytes = taskLastWrittenBytes[url] ?? 0
        let byteDelta = totalBytesWritten - lastBytes
        taskLastWrittenBytes[url] = totalBytesWritten
        
        // 更新累计完成字节
        objectCompletedBytes[objectId, default: 0] += byteDelta
        updateOverallProgress(for: objectId)
        
        // 更新单个任务的进度
        var updatedDownload = download
        updatedDownload.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        activeTasks[url] = updatedDownload
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        guard let url = downloadTask.originalRequest?.url,
              var download = activeTasks[url] else { return }
        
        let objectId = download.object.id
        // 确保任务完成时的字节数被完全统计
        let totalBytesWritten = downloadTask.countOfBytesReceived
        let lastBytes = taskLastWrittenBytes[url] ?? 0
        let byteDelta = totalBytesWritten - lastBytes
        objectCompletedBytes[objectId, default: 0] += byteDelta
        
        updateOverallProgress(for: objectId)
        
        // 标记任务完成
        download.progress = 1.0
        download.isActive = false
        activeTasks[url] = download
        
        // 清理缓存
        taskLastWrittenBytes.removeValue(forKey: url)
        
        // 文件移动等逻辑...
    }
    
    private func updateOverallProgress(for objectId: MyObject.ID) {
        guard let totalBytes = objectTotalBytes[objectId], totalBytes > 0,
              let completedBytes = objectCompletedBytes[objectId] else { return }
        DispatchQueue.main.async {
            self.objectOverallProgress[objectId] = Float(completedBytes) / Float(totalBytes)
        }
    }
}

关键注意事项

  1. 线程安全:URLSession的回调在后台线程执行,更新@Published变量必须切换到主线程(使用DispatchQueue.main.async);
  2. 断点续传处理:初始化已有任务时,要同步缓存已下载的字节数和进度,避免进度重置;
  3. 异常场景:若遇到totalBytesExpectedToWrite为0的情况(未知文件大小),可 fallback 到平均进度逻辑,或单独标记该任务进度为未知;
  4. 内存管理:任务完成后及时清理对应缓存项,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Leo Klaus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 06:24:58