如何基于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) } } }
关键注意事项
- 线程安全:URLSession的回调在后台线程执行,更新
@Published变量必须切换到主线程(使用DispatchQueue.main.async); - 断点续传处理:初始化已有任务时,要同步缓存已下载的字节数和进度,避免进度重置;
- 异常场景:若遇到
totalBytesExpectedToWrite为0的情况(未知文件大小),可 fallback 到平均进度逻辑,或单独标记该任务进度为未知; - 内存管理:任务完成后及时清理对应缓存项,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Leo Klaus
相关产品推荐
相关产品推荐

