解压文件前的额外检查:下载完成后立即解压是否正确?
关于文件下载后立即解压的逻辑疑问与优化建议
核心问题解答
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
相关产品推荐
相关产品推荐

