ARTICLE · 1019403
iOS 如何实现 ZIP / RAR / 7z 解压?从压缩格式、流式解压到密码压缩架构
iOS 如何实现 ZIP / RAR / 7z 解压?从压缩格式、流式解压到密码压缩架构
在前面的文章中,我们已经把文件浏览器的基础架构逐渐搭起来:
01 iOS 文件浏览器架构02 GB 级大文件处理03 Wi-Fi 文件传输04 SMB / NAS05 WebDAV06 FTP
从这一篇开始,我们回到文件浏览器最常见、也是最容易被普通用户感知到的一类功能:
压缩与解压
很多用户安装文件管理器,第一次真正用到它,可能就是因为收到一个:
archive.zip或者:
project.rar甚至:
backup.7ziPhone 上打不开。
于是用户真正的问题非常简单:
这个压缩包怎么打开?
但对于开发者来说,背后并不只是调用一个:
unzip()真正需要处理的问题包括:
ZIP、RAR、7z 有什么区别 TAR、GZIP 为什么又不一样 密码压缩如何处理 大压缩包如何避免爆内存 解压进度如何计算 用户取消以后怎么办 同名文件如何解决冲突 如何防止 Zip Slip 路径穿越 如何防止压缩炸弹 压缩包里面几万个文件怎么办 如何支持预览而不全部解压 如何把解压任务纳入 FileOperationManager
所以这一篇我们从架构角度拆解:
一个真正可靠的 iOS 压缩解压模块应该如何设计?
1. “压缩包”其实不是一种格式
从用户角度:
.zip.rar.7z
都可以统称:
压缩包。
但是技术上,它们是完全不同的格式。
例如:
ZIPRAR7zTARGZIPXZBZIP2
它们在:
文件结构 压缩算法 加密方式 索引方式 随机访问能力 多文件支持
等方面都有明显区别。
因此:
ArchiveService 不应该建立在“所有压缩包都一样”的假设上。
2. ZIP 是什么?
ZIP 是最常见的归档格式之一。
它可以同时:
Archive+Compress
也就是说,它既可以:
把多个文件装进一个容器
又可以:
对其中的数据进行压缩。
例如:
project.zip│├── README.md├── Images/│ ├── 001.jpg│ └── 002.jpg└── Documents/└── report.pdf
ZIP 很大的优势是:
CompatibilityWindows、macOS、iOS 以及各种文件工具都普遍支持。
所以如果用户问:
应该把文件压缩成什么格式方便分享?
很多时候 ZIP 都是很安全的选择。
3. RAR 又是什么?
RAR 同样是一种归档格式。
用户在 Windows 环境中尤其容易遇到:
movie.rarsoftware.rarphotos.rar
RAR 在压缩率、分卷等方面拥有自己的格式能力。
但对于 App 开发来说,一个重要现实是:
RAR 并不像 ZIP 那样可以简单地认为“系统天然支持”。
开发者通常需要使用相应的解析/解压实现,并且需要认真确认所使用实现的:
LicenseDistribution TermsFormat Support
尤其如果要把功能集成到商业 App 中,不能只看:
GitHub 上能不能跑。
还要确认:
这个库是否允许以你的商业方式进行分发和使用。
4. 7z 为什么经常压得更小?
7z 是另一种常见归档格式。
它经常搭配:
LZMALZMA2
等压缩算法使用。
很多场景中,7z 可以获得比较高的压缩率。
于是用户经常会看到:
1.2GB Folder↓7z↓700MB Archive
当然:
压缩率不是免费的。
通常需要在:
Compression Ratio↕CPU Time↕Memory
之间进行权衡。
压得越狠,并不意味着在移动设备上体验一定越好。
5. TAR 和 ZIP 其实不是同一类东西
很多开发者第一次看到:
.tar.gz会把它理解成一种“压缩格式”。
其实更准确地说:
TAR主要负责:
Archive
也就是:
把很多文件打包成一个连续的数据流。
例如:
Folder/├── A├── B└── C
先:
TAR↓archive.tar
再:
GZIP↓archive.tar.gz
所以:
.tar.gz可以理解为:
Archive+Compression
两个步骤组合。
这和 ZIP:
Archive + Compression集成在同一个格式里的思路并不完全一样。
6. 因此需要统一的 Archive Format 模型
不要在业务代码里到处写:
if ext == "zip" { ... }else if ext == "rar" { ... }else if ext == "7z" { ... }
更合理的是先建立统一模型:
enum ArchiveFormat {case zipcase rarcase sevenZipcase tarcase gzipcase bzip2case xz}
然后:
File↓ArchiveDetector↓ArchiveFormat↓ArchiveService
格式判断也不要完全依赖:
File Extension更可靠的实现可以结合:
Extension+Magic Number+Header
判断。
因为一个文件叫:
photo.zip并不代表里面一定真的是 ZIP。
7. ArchiveService 应该和 FileProvider 分开
前几篇我们一直在构建:
FileProvider它负责:
LocalSMBWebDAVFTP
这些“文件来自哪里”的问题。
ArchiveService 解决的是:
拿到文件以后怎么处理。
所以:
FileProvider↓FileItem↓ArchiveService
而不是:
LocalZipProviderSMBZipProviderFTPZipProvider
否则组合会迅速爆炸。
架构应该保持:
Source↓FileProvider↓FileItem↓ArchiveService
8. 可以定义统一的 ArchiveService
例如:
protocol ArchiveService {func listEntries(in archive: FileItem) async throws -> [ArchiveEntry]func extract(archive: FileItem,to destination: FileLocation) async throwsfunc createArchive(from items: [FileItem],format: ArchiveFormat,to destination: FileLocation) async throws}
核心抽象:
ArchiveService│├── List├── Extract└── Compress
至于底层:
ZIPRAR7z
应该由具体实现决定。
9. 为什么“查看压缩包内容”和“解压”要分开?
很多用户只是想:
看看压缩包里面有什么。
如果一个:
15GB archive.zip用户点一下就自动全部解压,
体验显然很差。
所以应该支持:
Archive↓Read Metadata / Index↓Show Entries
例如:
project.zip│├── README.md├── Photos/├── Documents/└── source/
用户可以先浏览。
需要某个文件时再:
Extract Selected这意味着 ArchiveService 至少需要:
List Entries和:
Extract两套能力。
10. ArchiveEntry 应该独立建模
例如:
struct ArchiveEntry {let path: Stringlet name: Stringlet isDirectory: Boollet compressedSize: Int64?let uncompressedSize: Int64?let modifiedDate: Date?}
UI 看到的是:
ArchiveEntry而不是具体:
ZIP Central Directory RecordRAR Header7z Entry
这样不同格式就可以共享:
Archive Browser UI11. 解压大文件也必须考虑内存
假设压缩包里有:
video.mp420GB
错误思路:
Compressed Entry↓Decompress↓20GB Data↓Memory
基本不可接受。
正确方向:
Compressed Stream↓Decompressor↓Chunk↓Destination File
核心仍然是:
Streaming。
也就是说:
Archive Size = 30GB不应该意味着:
Memory = 30GB12. 解压实际上是一条数据管线
可以把一个文件的解压理解为:
Archive File↓Compressed Bytes↓Decoder↓Uncompressed Bytes↓Chunk↓Output File
所以可以进一步设计:
ArchiveReader↓Decompressor↓OutputWriter
这和前面的大文件传输架构非常相似。
区别只是中间多了:
Decompression13. 解压进度应该怎么算?
表面上很简单:
processed / total但这里有两个不同的 total:
Compressed Size和:
Uncompressed Size例如:
archive.7zCompressed:1GBUncompressed:5GB
进度可以基于:
Processed EntriesProcessed Compressed BytesProcessed Uncompressed Bytes
不同库能够提供的信息不同。
对于用户而言,更重要的是:
Extracting...2.6 GB / 5 GB52%
所以 Archive Operation 最好把底层进度统一映射成:
0.0 ... 1.0而不是让 UI 理解每一种格式的内部细节。
14. 一个压缩包可能有几十万个文件
比如:
node_modules.zip里面可能有海量小文件。
这种压缩包和:
一个 20GB 视频是完全不同的压力模型。
前者压力来自:
File CountDirectory CreationMetadataSmall IO
后者压力来自:
Large Sequential IO因此 ArchiveService 不能只优化:
Large File还需要考虑:
Huge Entry Count15. 不要一次把 100,000 个 Entry 全塞进 UI
例如:
100,000 Archive Entries如果全部:
Parse↓Create Model↓Render
可能会造成明显性能压力。
所以可以考虑:
Lazy ParsingPaginationBatch UpdatesVirtualized List
和 NAS 大目录的思路一样:
内容很多,不代表首屏必须全部准备好。
16. 解压必须支持取消
用户开始:
Extract 50GB Archive到:
61%发现选错目录。
如果不能取消,只能干等。
所以 Archive Operation 需要:
Cancel例如每处理一定数量数据:
try Task.checkCancellation()逻辑:
Entry↓Chunk↓Check Cancel↓Chunk↓Check Cancel
最终:
Cancel↓Stop Decoder↓Close Files↓Cleanup
17. 取消以后已经解压的文件怎么办?
这是产品设计必须明确的问题。
假设:
archive.zip1000 Files
已经解压:
600 Files用户取消。
应该:
方案 A
全部删除:
Rollback方案 B
保留已经成功的文件。
方案 C
让用户选择。
对于普通文件管理器,我更倾向于:
默认尽量保证目标目录处于可理解状态。
一种实现方式是:
Extract↓Temporary Directory↓Complete↓Move to Final Destination
失败:
Temporary Directory↓Delete
成功:
Temporary Directory↓Finalize
这样事务边界更清晰。
18. 但是临时目录会占用双倍空间吗?
这是一个非常现实的问题。
如果压缩包解压后:
40GB临时目录再移动到最终目录,
如果底层移动可以通过同一卷上的 rename/move 完成,
通常不需要重新复制全部内容。
但如果:
Temporary和:
Destination位于不同 Provider 或不同文件系统,
可能就需要真实的数据迁移。
所以:
Transactional Extract也需要结合:
Destination Provider设计。
19. 同名文件冲突怎么办?
假设目标目录已经有:
report.pdf压缩包里也有:
report.pdf不能偷偷覆盖。
应该明确策略:
ReplaceKeep BothSkipCancel
如果是整个压缩包:
Apply to All也很重要。
例如:
[✓] Apply to all conflicts否则一个 1000 文件压缩包可能连续弹几十次。
20. Keep Both 要生成安全的新名字
例如:
report.pdf已经存在。
生成:
report (1).pdfreport (2).pdf
但注意:
Directory也可能冲突。
所以冲突策略应该属于:
FileOperation / Destination Resolver而不是 ZIP 解压器内部随便处理。
这样:
CopyMoveExtractDownload
都可以共享冲突逻辑。
21. 密码压缩怎么处理?
例如用户打开:
private.zipArchiveService 发现:
EncryptedUI 显示:
Enter Password用户输入:
********然后:
Password↓Archive Decoder↓Decrypt↓Decompress
密码不要:
写入普通日志也不要:
长期放在普通 UserDefaults如果用户选择:
记住这个压缩包密码
可以考虑安全存储。
22. ZIP 加密也有不同类型
看到:
Password Protected ZIP不能假设所有密码 ZIP 都一样。
历史上 ZIP 存在较旧的传统加密方式。
现代工具也可能使用:
AES等更强的加密方案。
所以 App 声称支持:
Encrypted ZIP时,需要明确实际底层实现支持哪些加密方式。
否则很容易出现:
某些密码 ZIP 能开某些不能开
用户会以为是 Bug。
23. 7z 密码还有一个特殊点:文件名也可能被隐藏
有些加密归档不仅:
File Data被加密。
连:
File Names也可能受到保护。
这意味着用户甚至无法先浏览:
Archive Entries而需要:
Password↓Read Header↓Show Entries
所以 Archive Browser 需要允许:
Locked这种状态。
24. 密码错误应该是独立错误类型
不能:
Extract Failed就结束。
应该区分:
Wrong PasswordUnsupported EncryptionCorrupted ArchiveNot Enough SpaceCancelledPermission Denied
例如:
enum ArchiveError: Error {case wrongPasswordcase unsupportedFormatcase unsupportedEncryptioncase corruptedArchivecase insufficientStoragecase invalidEntryPathcase cancelledcase unknown(Error)}
这样 UI 才能告诉用户:
密码错误,请重新输入。
而不是:
解压失败。
25. Zip Slip:压缩解压里非常重要的安全问题
假设一个恶意 ZIP 内部路径:
../../../../Library/secret.txt如果解压逻辑只是:
destination.appendingPathComponent(entry.path)
然后直接写文件,
攻击者就可能尝试把文件写出目标目录。
这类问题通常被称为:
Zip Slip / Path Traversal。
26. 每一个 Entry Path 都必须验证
例如目标:
/Documents/Extracted/Entry:
../../evil.txt拼接后:
/Documents/Extracted/../../evil.txt如果直接规范化,
可能逃离:
Extracted/所以必须:
Entry Path↓Normalize↓Resolve↓Check Destination Root↓Allowed?
只有:
Final Path ∈ Destination Root才允许写入。
核心原则:
压缩包里的路径永远不能被当成可信输入。
27. 绝对路径同样不能信
例如 Entry:
/private/var/...或者某些奇怪的 Windows 风格路径:
C:\...都应该在 Archive 层做标准化和拒绝。
不能让归档文件自己决定:
我要写到设备上的哪里。
归档只应该影响:
Destination Root下面的相对路径。
28. 还要警惕符号链接
假设压缩包中创建:
link -> ../../somewhere后续 Entry 再写入:
link/file理论上可能绕过普通字符串路径检查。
因此如果底层格式支持:
Symlink解压器还需要明确策略:
RejectPreserve SafelyResolve Carefully
尤其文件管理 App 不应该默认盲目信任归档里的链接。
29. 什么是压缩炸弹?
压缩包还有另一类风险:
Decompression Bomb。
例如一个非常小的压缩包:
42KB解压后可能膨胀成:
Several GB甚至更大。
形式上:
Tiny Archive↓Huge Expansion↓Disk Full↓Resource Exhaustion
这类归档可能是恶意的,也可能只是极端压缩数据。
30. 不能只看压缩包本身大小
错误判断:
Archive = 10MB所以:
一定没问题。
实际上真正应该尽量估计:
Total Uncompressed Size例如:
Compressed:100MBUncompressed:80GB
就需要谨慎。
可以在解压前计算:
Estimated Output Size↓Available Storage↓Enough?
明显不够就提前失败。
31. 还要限制解压文件数量
压缩炸弹不一定只靠尺寸。
也可能是:
1,000,000 Tiny Files即使总大小不是特别大,
文件系统操作也可能造成巨大压力。
所以安全策略可以考虑:
Maximum Entry CountMaximum Expanded SizeMaximum Compression RatioMaximum Nested Depth
具体限制要结合产品场景设计。
32. 嵌套压缩包尤其危险
例如:
a.zip↓b.zip↓c.zip↓d.zip
如果 App 自动递归:
Auto Extract Everything风险会快速放大。
所以普通文件管理器最好不要:
发现压缩包就无限自动继续解。
更安全的是:
Extract Current Archive完成后由用户决定是否继续打开内部归档。
33. 压缩文件时也需要流式处理
创建:
backup.zip同样不能:
100 Files↓All Data in Memory↓Compress
更合理:
File A↓Stream↓Compressor↓ArchiveFile B↓Stream↓Compressor↓Archive
所以:
Compress和:
Extract都应该围绕:
Streaming Pipeline
设计。
34. 压缩等级应该让用户选择吗?
例如:
FastBalancedMaximum
对应:
Speed↕Compression Ratio
对于普通用户,直接展示:
Level 0-9未必友好。
可以转换成:
FastStandardBest Compression
高级设置里再提供更细控制。
这也是:
技术参数不一定应该原样暴露给用户。
35. 压缩视频往往不会小很多
用户可能选择:
movie.mp4然后 ZIP。
发现:
2.00GB↓1.98GB
因为 MP4 内部本身已经使用视频编码压缩。
类似还有:
JPEGHEICMP3AAC
它们本身已经高度压缩。
所以文件压缩和:
Video CompressionImage CompressionAudio Compression
不是一回事。
ZIP:
对文件数据进行归档/通用压缩。
视频压缩:
重新编码媒体内容。
这一点很适合在产品里给用户解释清楚。
36. ArchiveService 不应该负责视频转码
如果用户:
movie.mp4想从 2GB 变成 300MB,
应该进入:
MediaService而不是:
ArchiveService所以:
ArchiveService↓ZIP / 7z / TARMediaService↓Video / Audio / Image Compression
职责必须分开。
37. 压缩任务应该进入统一 FileOperationManager
例如用户:
Select 500 Files↓Compress↓archive.zip
这本质仍然是:
Long Running Operation应该拥有:
WaitingRunningProgressCancelCompletedFailed
所以可以:
OperationType.compressOperationType.extract
纳入:
FileOperationManager任务中心:
Creating archive.zip2.1 GB / 5.0 GB42%
38. Archive Operation 可以复用现有基础设施
前几篇已经拥有:
ProgressCancellationConflict HandlingTemporary FileError MappingStorage Check
压缩解压无需重新发明。
例如:
Extract↓FileOperationManager↓ArchiveService↓Temporary Destination↓Progress↓Finalize
这样架构才能长期维护。
39. 远程压缩包怎么办?
这是统一架构真正有意思的地方。
比如:
NAS↓backup.zip
用户想打开。
最简单的方法:
SMB↓Download Entire ZIP↓Local↓ArchiveService
但如果:
backup.zip = 50GB显然体验不好。
更高级的实现可以考虑:
Remote Range Read让 ArchiveService 通过:
FileProvider.read(offset:length:)按需读取。
40. 为什么 ZIP 很适合随机访问?
ZIP 的目录信息通常让客户端能够先获得:
Entry Index再读取特定 Entry 所需的数据区域。
如果 FileProvider 支持:
Random Access理论上就可以:
Remote ZIP↓Read Archive Index↓Show Entries↓User selects one file↓Read Required Range
而不是:
Download 50GB ZIP之后才能看到里面有什么。
这对 NAS / WebDAV 文件浏览器非常有价值。
41. 所以 ArchiveService 最终也需要抽象数据源
一开始可能是:
ArchiveService(URL)更进一步可以变成:
ArchiveDataSource例如:
protocol ArchiveDataSource {var size: Int64 { get }func read(offset: Int64,length: Int) async throws -> Data}
于是:
Local File↓LocalArchiveDataSourceSMB File↓RemoteArchiveDataSourceWebDAV↓RemoteArchiveDataSource
ArchiveService 不需要知道:
文件到底在哪。
它只需要:
给我指定范围的数据。
42. 这就是前面 FileProvider 抽象的延伸
整个链路可以变成:
SMB / WebDAV / Local↓FileProvider↓Random Access↓ArchiveDataSource↓ArchiveService↓ArchiveEntry
于是:
Remote Archive Preview就成为可能。
这也是为什么文件管理器底层抽象设计得好以后,新功能会越来越容易复用。
43. 创建密码 ZIP 时需要把“安全”当作功能的一部分
如果用户选择:
Create Encrypted ArchiveUI 不应该只提供:
Password最好还要处理:
Confirm Password以及:
是否显示密码 密码强度提示 忘记密码无法恢复的说明
因为压缩包密码和 App 登录密码完全不同。
如果归档格式本身不提供恢复机制:
用户忘记密码,App 通常也无法帮他恢复文件。
这一点必须提前说明。
44. Archive Password 可以进入专门的密码管理层
例如:
Archive Password↓Temporary Use
默认只在当前任务中存在。
如果用户明确选择:
Remember Password再进入安全存储。
例如:
Archive Password Store↓Keychain
这样和:
SMB CredentialWebDAV CredentialFTP Credential
在安全基础设施上可以复用。
45. 一个完整的 Archive 架构
最终可以整理成:
┌─────────────────────────────┐│ UI ││ ││ Archive Browser ││ Compress Sheet ││ Extract Progress │└──────────────┬──────────────┘│▼┌─────────────────────────────┐│ FileOperationManager ││ ││ Compress ││ Extract ││ Progress ││ Cancel ││ Conflict │└──────────────┬──────────────┘│▼┌─────────────────────────────┐│ ArchiveService ││ ││ Detect ││ List Entries ││ Extract ││ Create │└──────────────┬──────────────┘│┌──────┼───────┐▼ ▼ ▼ZIP RAR 7z│└──────┬───────┘▼ArchiveDataSource│┌─────────┼─────────┐▼ ▼ ▼Local SMB WebDAV
旁边还需要:
Password StoreSecurity ValidatorPath ValidatorStorage CheckerConflict Resolver
46. TS File Explorer 为什么需要统一 ArchiveService?
从开发者角度:
ZIPRAR7zTARGZIP
都是完全不同的格式。
但用户根本不想了解这些。
用户只想:
收到文件以后能打开。
例如:
微信 / 邮件 / 网盘↓project.rar↓TS File Explorer↓查看内容↓Extract↓使用文件
用户真正需要的不是:
一个 RAR Decoder。
而是:
一个能够处理压缩文件的统一入口。
这也是文件浏览器产品非常重要的一层价值。
47. 压缩功能也应该遵循“协议差异隐藏在底层”
就像:
SMBWebDAVFTP
最终统一成:
FileProvider一样。
ZIPRAR7zTAR
最终也应该统一成:
ArchiveService于是用户看到的永远只有:
OpenPreviewExtractCompressPassword
而不是:
这个格式走 A API那个格式走 B API
48. 开发压缩解压功能前,建议先回答这 20 个问题
如果准备实现完整的 Archive 模块,建议先回答:
1. 支持哪些格式?2. 格式如何检测?3. 是否支持只查看内容?4. 是否支持选择性解压?5. 大文件是否流式处理?6. 解压进度如何计算?7. 压缩进度如何计算?8. 用户取消以后如何清理?9. 同名文件怎么处理?10. 是否支持密码 ZIP?11. 支持哪些加密方式?12. 密码如何保存?13. 如何防止 Zip Slip?14. 如何处理 Symlink?15. 如何防止压缩炸弹?16. 如何处理几十万个 Entry?17. 是否支持远程 Archive?18. 是否支持 Random Access?19. Archive Operation 如何进入任务中心?20. 第三方库的 License 是否适合商业分发?
如果这些问题没有提前考虑,压缩解压很容易从:
一个简单功能变成:
性能问题+安全问题+兼容问题+大量格式特殊逻辑
写在最后
ZIP / RAR / 7z 看起来只是:
CompressExtract
两个按钮。
真正实现以后,会发现它背后实际上包含:
Format Detection+Streaming IO+Encryption+Progress+Cancellation+Conflict Handling+Path Security+Resource Protection
所以对于文件浏览器来说:
Archive 模块不应该只是几个第三方解压 API 的集合,而应该是一套独立的文件处理系统。
到目前为止,这个系列已经从:
File Browser逐渐发展成:
File Platform整个路线:
01 File Browser Architecture↓02 Large File IO↓03 Wi-Fi Transfer↓04 SMB / NAS↓05 WebDAV↓06 FTP↓07 ZIP / RAR / 7z
下一篇我建议进入另一个和 TS File Explorer 很匹配、同时非常适合普通用户搜索的主题:
《iOS 图片格式转换怎么实现?HEIC、JPEG、PNG、WebP 与图片压缩架构》
下一篇可以系统讲:
HEIC → JPGPNG → JPGWebP → PNG图片压缩分辨率调整批量处理EXIF内存峰值
这样内容会从“文件协议”进一步扩展到真正的:
文件处理能力。