问题描述
在使用Zip库压缩文件时,发现压缩并下载的文件比较大时(100MB以上),应用就会卡住,鼠标箭头呈现彩虹轮盘的样式。

我想要实现的效果是,Zip库在解压时,可以通过回调闭包的返回进度:
try Zip.zipFiles(paths: ImagesURL, zipFilePath: destinationURL, password: nil) { progress in
print("进度:\(progress)") // 0.1 0.22 0.4 0.7 0.9 1.0
}
例如,每秒钟返回当前的解压缩进度,根据进度显示解码的动画。
因此,我尝试使用await/async包裹zipImages方法,让压缩和下载变成异步执行。
func zipImages() async {
do {
print("zipImages 任务中是否是主线程?", Thread.isMainThread)
let downloadsDirectory = FileManager.default.urls(for:.downloadsDirectory, in: .userDomainMask)[0]
let destinationURL = downloadsDirectory.appendingPathComponent("ImageSlim.zip")
var ImagesURL:[URL] {
let urls = appStorage.images.compactMap { $0.outputURL }
return urls
}
await MainActor.run {
showDownloadsProgress = true
}
try Zip.zipFiles(paths: ImagesURL, zipFilePath: destinationURL, password: nil) { progress in
print("zipFiles 任务中是否是主线程?", Thread.isMainThread)
self.progress = progress
if progress == 1 {
showDownloadsProgress = false
}
}
print("打包完成")
} catch {
print("打包失败")
}
}
虽然使用async/await,但是仍然存在压缩和下载大文件时,应用卡住,鼠标指针呈现彩虹轮盘。
在添加输出语句Thread.isMainThread,判断是否为主线程时,发现压缩仍然仍然是在主线程调用的:
zipImages 任务中是否是主线程? true
zipFiles 任务中是否是主线程? true
async/await并没有实现异步调用,这表明Zip本身并不支持async闭包,当压缩和下载时就会阻塞当前线程,从而导致卡住的场景。
解决方案
我们需要在后台线程执行zip操作,当进度更新时再切换回主线程:
func zipImages() async {
showDownloadsProgress = true // 显示进度条
progress = 0.0 // 重置压缩进度条的数值
DispatchQueue.global(qos: .userInitiated).async { // 使用后台线程调用
do {
print("zipImages 任务中是否是主线程?", Thread.isMainThread)
let downloadsDirectory = FileManager.default.urls(for:.downloadsDirectory, in: .userDomainMask)[0]
let destinationURL = downloadsDirectory.appendingPathComponent("ImageSlim.zip")
var ImagesURL:[URL] {
let urls = appStorage.images.compactMap { $0.outputURL }
return urls
}
try Zip.zipFiles(paths: ImagesURL, zipFilePath: destinationURL, password: nil) { progress in
DispatchQueue.main.async { // 切换回主线程,更新进度条的进度。
self.progress = progress
if progress == 1 {
showDownloadsProgress = false
}
}
}
DispatchQueue.main.async { // 切换回主线程,输出信息
print("打包完成")
}
} catch {
DispatchQueue.main.async { // 切换回主线程,隐藏进度条
self.showDownloadsProgress = false
print("打包失败")
}
}
}
}
这里主要知识点有两个:
1、使用后台线程处理Zip库的压缩任务:
DispatchQueue.global(qos: .userInitiated).async { Zip库代码 }
2、需要更新进度条UI时,切换回主线程更新:
DispatchQueue.main.async { // 切换回主线程,隐藏进度条
self.showDownloadsProgress = false
print("打包失败")
}
这样就可以实现后台线程处理复杂的工作任务,又可以避免UI卡顿的问题。

总结
因为需要执行比较复杂、耗时的任务,因此需要切换到后台线程执行,否则这些复杂的任务就会占用主线程,从而导致UI卡顿。

如果主线程去处理复杂、耗时的任务,势必会导致UI的卡顿。

因此,后台线程就是用于处理这一场景。
最后,说明一下为什么使用DispatchQueue.global(qos: .userInitiated).async处理后台线程的任务,而不是使用其他的线程模型。
因为全局线程是由系统提供的线程池,用于处理后台任务。全局线程根据任务优先级分为以下六种:
1、.userInteractive: 优先级最高,适用于需要立即完成的短任务(例如动画或用户交互)。
2、.userInitiated: 高优先级,适用于用户主动触发的短期任务,例如用户点击按钮后需要立即执行的任务。
3、.default:默认优先级,适用于没有明确优先级的任务。
4、.utility: 中低优先级,适用于需要耗时较长但对性能要求不高的任务,例如下载文件或处理数据。
5、.background: 优先级最低,适用于后台任务(例如文件预处理)。
6、.unspecified:不指定优先级,系统会决定使用的服务质量。
这里的压缩场景适合.userInitiated,表示拥护期待尽快完成的任务,例如打包ZIP、保存文件、网络加载等。
如果是普通的后台任务,可以使用.default,下载文件、导入大量数据等复杂任务,可以使用 .utility,因此可以根据优先级设置对应的全局线程。
详细内容可以看一下《Swift中的线程模型》文章的全局线程部分。
相关文章
1、Swift管理多线程任务框架GCD:https://fangjunyu.com/2024/11/02/swift%e7%ae%a1%e7%90%86%e5%a4%9a%e7%ba%bf%e7%a8%8b%e4%bb%bb%e5%8a%a1%e6%a1%86%e6%9e%b6gcd/
2、Swift中的线程模型:https://fangjunyu.com/2024/12/25/swift%e4%b8%ad%e7%9a%84%e7%ba%bf%e7%a8%8b%e6%a8%a1%e5%9e%8b/
3、Swift解压ZIP文件:https://fangjunyu.com/2025/04/09/swift%e8%a7%a3%e5%8e%8bzip%e6%96%87%e4%bb%b6/