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

解压文件前的额外检查:下载完成后立即解压是否正确?

关于文件下载后立即解压的逻辑疑问与优化建议

核心问题解答

1. 收到didFinishDownloadingTo回调后立即启动解压是否正确?

基础逻辑成立,但现有实现有优化空间。didFinishDownloadingTo触发时,系统已将下载的临时文件写入指定临时路径,理论上可以立即处理。但你的代码把解压操作放在了主队列中,而解压属于耗时IO操作,会阻塞UI线程导致卡顿,建议移到后台队列执行。

2. 解压前需要执行哪些额外检查?

必须补充完善检查逻辑,现有检查仅验证目录存在,远远不够:

  • 验证目标zip文件存在性:你当前硬编码1.zip作为解压源,但实际下载的是0.zip,且未校验文件是否真的存在于目标目录,这会直接导致解压失败。
  • 验证文件完整性:可通过对比下载任务响应头的Content-Length与实际文件大小,或结合服务器提供的MD5/SHA校验值,避免下载过程中文件损坏。
  • 校验zip文件有效性:通过读取文件头(标准zip文件开头为PK\x03\x04),提前过滤非zip格式的文件,避免解压时抛出无效格式错误。
  • 检查目录权限与可写性:确认目标目录具备读写权限,防止因权限不足导致解压失败。

3. 下载成功后无法启动解压的异常场景

这类场景确实存在,常见的有:

  • 文件移动失败:DownloadManager中moveItem操作可能因磁盘空间不足、目标文件被占用、权限限制等原因失败,但当前代码仍会触发onSuccess回调,导致解压时找不到文件。
  • 文件名匹配错误:硬编码解压源文件名,与实际下载的文件名不一致,直接导致文件找不到。
  • zip文件损坏:网络波动导致下载的文件不完整,虽然URLSession会自动重试,但极端情况下仍可能拿到损坏的zip包。
  • 目录创建失败:createDirectory操作可能因系统限制失败,但未做错误判断,后续解压路径无效。

代码优化建议

ViewController 优化

将解压移到后台队列,替换硬编码文件名,完善检查逻辑:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    
    let url = URL(string: "0.zip")!
    let downloadManager = DownloadManager()
    downloadManager.identifier = indexPath.row + 1
    downloadManager.collectionId = 0
    downloadManager.folderPath = "\(indexPath.row + 1)"
    let downloadTaskLocal = downloadManager.activate().downloadTask(with: url)
    downloadTaskLocal.resume()
                
    downloadManager.onSuccess = { [weak self] row, fileName in
        guard let self = self else { return }
        // 更新UI在主队列
        DispatchQueue.main.async {
            self.items[row - 1].state = .completed
            self.reloadItem(indexPath: .init(row: row - 1, section: 0))
        }
        
        // 后台队列执行解压
        DispatchQueue.global(qos: .background).async {
            let fileManager = FileManager.default
            guard let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
                print("无法获取Documents目录")
                return
            }
            let destDirURL = URL(fileURLWithPath: docDir).appendingPathComponent("\(row)")
            let sourceZipURL = destDirURL.appendingPathComponent(fileName)
            
            // 检查zip文件是否存在
            guard fileManager.fileExists(atPath: sourceZipURL.path) else {
                print("zip文件不存在")
                return
            }
            
            // 校验zip文件头
            guard let fileHandle = try? FileHandle(forReadingFrom: sourceZipURL),
                  let header = fileHandle.readData(ofLength: 4).map({ String(format: "%02X", $0) }).joined().uppercased() == "504B0304" else {
                print("不是有效的zip文件")
                return
            }
            
            let unzipProgress = Progress()
            let observation = unzipProgress.observe(\.fractionCompleted) { progress, _ in
                print("解压进度: ", progress.fractionCompleted)
                if progress.fractionCompleted == 1.0 {
                    DispatchQueue.main.async {
                        self.items[row - 1].state = .unzipped
                        self.reloadItem(indexPath: .init(row: row - 1, section: 0))
                    }
                }
            }
            
            do {
                try fileManager.unzipItem(at: sourceZipURL, to: destDirURL, progress: unzipProgress)
            } catch {
                print("解压失败: \(error.localizedDescription)")
            }
            
            observation.invalidate()
        }
    }
}

DownloadManager 优化

修复强制解包问题,补充错误处理,传递实际文件名到回调:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
    // 避免强制解包,处理可选值
    guard let stringNumb = session.accessibilityHint, let someNumb = Int(stringNumb),
          let string1 = session.sessionDescription, let some1 = Int(string1),
          var queue = downloadQueue[someNumb],
          let idx = queue.index(of: some1) else {
        print("无法处理下载队列")
        return
    }
    
    queue.remove(at: idx)
    downloadQueue[someNumb] = queue
    print("移除后队列:\(downloadQueue)")
    
    guard let fileName = downloadTask.originalRequest?.url?.lastPathComponent else {
        print("无法获取文件名")
        return
    }
    
    guard let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
        print("无法获取Documents目录")
        return
    }
    
    let fileManager = FileManager.default
    let destDirURL = URL(fileURLWithPath: docDir).appendingPathComponent(folderPath)
    
    do {
        try fileManager.createDirectory(at: destDirURL, withIntermediateDirectories: true, attributes: nil)
        let destFileURL = destDirURL.appendingPathComponent(fileName)
        // 若目标文件已存在,先删除
        if fileManager.fileExists(atPath: destFileURL.path) {
            try fileManager.removeItem(at: destFileURL)
        }
        try fileManager.moveItem(at: location, to: destFileURL)
        // 传递文件名到回调
        onSuccess?(identifier, fileName)
    } catch {
        print("文件移动失败: \(error.localizedDescription)")
        // 移动失败时不触发成功回调
    }
}

// 同时更新onSuccess的定义,添加fileName参数
var onSuccess: ((Int, String) -> Void)?

额外注意事项

  • 线程管理:解压、文件校验等耗时操作必须放在后台队列,避免阻塞主队列。
  • 错误日志:不要仅打印"error",要输出具体错误描述,方便定位问题。
  • 重复下载处理:添加重复点击判断,避免重复创建下载任务和DownloadManager实例。
  • 内存泄漏:确保Progress的观察者正确调用invalidate,避免内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 00:50:07