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

Swift中如何将NSURLSession多文件下载进度聚合为总进度?

解决NSURLSession多文件下载合并总进度的问题

这个问题我之前也碰到过,核心就是要统一统计所有下载任务的总字节数和已下载字节数,而不是单个任务的进度比例直接返回。下面给你一步步拆解实现方案:

核心思路

  1. 为每个下载任务单独记录「总预期下载字节数」和「已下载字节数」
  2. 维护全局的「总预期字节数」和「总已下载字节数」
  3. 总进度 = 总已下载字节数 / 总预期字节数(仅当总预期字节数>0时计算)
  4. 只有当所有任务都完成/失败,且总已下载字节数等于总预期字节数时,才会显示100%

修改后的DownloadManager实现

import Foundation

// 存储单个下载任务的进度信息
struct DownloadTaskInfo {
    var totalBytesExpected: Int64
    var bytesReceived: Int64
}

class DownloadManager: NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
    // 单例实例
    static let shared = DownloadManager()
    
    // URLSession实例
    private var session: URLSession!
    // 存储每个任务的进度信息,key为taskIdentifier
    private var tasksInfo: [Int: DownloadTaskInfo] = [:]
    // 全局总预期字节数
    private var totalExpectedBytes: Int64 = 0
    // 全局总已下载字节数
    private var totalReceivedBytes: Int64 = 0
    // 进度回调闭包,返回0.0~1.0的进度值
    var progressHandler: ((Double) -> Void)?
    
    private override init() {
        super.init()
        // 初始化URLSession
        let config = URLSessionConfiguration.background(withIdentifier: "com.yourdomain.DownloadManager")
        session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }
    
    // 添加下载任务的方法
    func addDownloadTask(with url: URL) {
        let task = session.downloadTask(with: url)
        task.resume()
    }
    
    // MARK: - URLSessionDownloadDelegate
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 任务完成,更新总已下载字节数(确保和总预期一致)
        guard let taskInfo = tasksInfo[downloadTask.taskIdentifier] else { return }
        totalReceivedBytes += (taskInfo.totalBytesExpected - taskInfo.bytesReceived)
        
        // 移除已完成的任务信息
        tasksInfo.removeValue(forKey: downloadTask.taskIdentifier)
        
        // 计算并回调最终进度
        updateTotalProgress()
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let taskId = downloadTask.taskIdentifier
        
        // 首次获取任务总大小,初始化任务信息并累加总预期字节数
        if tasksInfo[taskId] == nil {
            let taskInfo = DownloadTaskInfo(totalBytesExpected: totalBytesExpectedToWrite, bytesReceived: totalBytesWritten)
            tasksInfo[taskId] = taskInfo
            totalExpectedBytes += totalBytesExpectedToWrite
        } else {
            // 更新任务的已下载字节数
            var taskInfo = tasksInfo[taskId]!
            taskInfo.bytesReceived = totalBytesWritten
            tasksInfo[taskId] = taskInfo
            
            // 更新全局总已下载字节数(这里要注意:首次初始化已经加过一次,后续只需要加本次写入的字节数)
            totalReceivedBytes += bytesWritten
        }
        
        // 计算并回调总进度
        updateTotalProgress()
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        let taskId = task.taskIdentifier
        guard let taskInfo = tasksInfo[taskId] else { return }
        
        if let error = error {
            // 任务失败,移除任务信息,并调整总预期和已下载字节数
            totalExpectedBytes -= taskInfo.totalBytesExpected
            totalReceivedBytes -= taskInfo.bytesReceived
            tasksInfo.removeValue(forKey: taskId)
            
            print("下载失败:\(error.localizedDescription)")
        }
        
        // 回调进度
        updateTotalProgress()
    }
    
    // MARK: - 私有方法:计算并回调总进度
    private func updateTotalProgress() {
        var progress: Double = 0.0
        
        if totalExpectedBytes > 0 {
            progress = Double(totalReceivedBytes) / Double(totalExpectedBytes)
            // 确保进度不会超过1.0(比如网络波动导致的字节统计误差)
            progress = min(progress, 1.0)
        }
        
        // 回到主线程回调(因为代理方法在后台线程执行)
        DispatchQueue.main.async {
            self.progressHandler?(progress)
        }
    }
}

关键代码解释

  1. DownloadTaskInfo结构体:专门存储单个任务的下载状态,避免全局变量混乱。
  2. tasksInfo字典:通过taskIdentifier作为key,快速定位每个任务的信息。
  3. totalExpectedBytes & totalReceivedBytes:全局统计所有任务的总大小和已下载量,这两个值是计算总进度的核心。
  4. updateTotalProgress方法:统一处理进度计算和回调,确保在主线程更新UI(因为URLSession的代理方法默认在后台线程执行)。
  5. 错误处理:在didCompleteWithError中处理任务失败的情况,及时从全局统计中移除失败任务的字节数,避免进度计算错误。

使用示例

// 初始化并设置进度回调
DownloadManager.shared.progressHandler = { progress in
    print("总下载进度:\(String(format: "%.2f%%", progress * 100))")
    // 更新UI,比如progressView.progress = progress
}

// 添加多个下载任务
if let url1 = URL(string: "https://example.com/file1.zip") {
    DownloadManager.shared.addDownloadTask(with: url1)
}
if let url2 = URL(string: "https://example.com/file2.zip") {
    DownloadManager.shared.addDownloadTask(with: url2)
}

这样实现后,只有当所有下载任务都完成(没有失败)时,总进度才会达到100%;如果有任务失败,总进度会基于剩余完成的任务重新计算,不会出现单个任务完成就跳100%的情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 04:24:52