夜雨聆风学习资料网

ARTICLE · 1019403

iOS 如何实现 ZIP / RAR / 7z 解压?从压缩格式、流式解压到密码压缩架构

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.7z

iPhone 上打不开。

于是用户真正的问题非常简单:

这个压缩包怎么打开?

但对于开发者来说,背后并不只是调用一个:

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 很大的优势是:

Compatibility

Windows、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 zip    case rar    case sevenZip    case tar    case gzip    case bzip2    case 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 archiveFileItem    ) async throws -> [ArchiveEntry]    func extract(        archiveFileItem,        to destinationFileLocation    ) async throws    func createArchive(        from items: [FileItem],        formatArchiveFormat,        to destinationFileLocation    ) 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 pathString    let nameString    let isDirectoryBool    let compressedSizeInt64?    let uncompressedSizeInt64?    let modifiedDateDate?}

UI 看到的是:

ArchiveEntry

而不是具体:

ZIP Central Directory RecordRAR Header7z Entry

这样不同格式就可以共享:

Archive Browser UI

11. 解压大文件也必须考虑内存

假设压缩包里有:

video.mp420GB

错误思路:

Compressed Entry      ↓Decompress      ↓20GB Data      ↓Memory

基本不可接受。

正确方向:

Compressed Stream       ↓Decompressor       ↓Chunk       ↓Destination File

核心仍然是:

Streaming。

也就是说:

Archive Size = 30GB

不应该意味着:

Memory = 30GB

12. 解压实际上是一条数据管线

可以把一个文件的解压理解为:

Archive File     ↓Compressed Bytes     ↓Decoder     ↓Uncompressed Bytes     ↓Chunk     ↓Output File

所以可以进一步设计:

ArchiveReader      ↓Decompressor      ↓OutputWriter

这和前面的大文件传输架构非常相似。

区别只是中间多了:

Decompression

13. 解压进度应该怎么算?

表面上很简单:

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 Count

15. 不要一次把 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.zip

ArchiveService 发现:

Encrypted

UI 显示:

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 ArchiveErrorError {    case wrongPassword    case unsupportedFormat    case unsupportedEncryption    case corruptedArchive    case insufficientStorage    case invalidEntryPath    case cancelled    case 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(        offsetInt64,        lengthInt    ) 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 Archive

UI 不应该只提供:

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内存峰值

这样内容会从“文件协议”进一步扩展到真正的:

文件处理能力。

相关学习资料

返回首页浏览学习资料