Android 解压zip文件你知道多少?
对于Android常用的压缩格式ZIP,你了解多少?
Android的有两种解压ZIP的方法,你知道吗?
ZipFile和ZipInputStream的解压效率,你对比过吗?
带着以上问题,现在就开始ZIP的解压之旅。
1. Zip文件结构
ZIP文件结构如下图所示, File Entry表示一个文件实体,一个压缩文件中有多个文件实体。
文件实体由一个头部和文件数据组,Central Directory由多个File header组成,每个File header都保存一个文件实体的偏移,文件最后由End of central directory结束。
1.1 Local File Header
|
偏移 |
字节数 |
描述 |
|
0 |
4 |
固定值0x04034b50 |
|
4 |
2 |
解压缩版本 |
|
6 |
2 |
标志 |
|
8 |
2 |
压缩方式 |
|
10 |
2 |
文件最后修改时间 |
|
12 |
2 |
文件最后修改日期 |
|
14 |
4 |
CRC-32校验 |
|
18 |
4 |
压缩后大小 |
|
22 |
4 |
压缩前大小 |
|
26 |
2 |
文件名称长度(n) |
|
28 |
2 |
扩展字段长度(m) |
|
30 |
n |
文件名称 |
|
30+n |
m |
扩展字段 |
1.2. Data descriptor
当头部标志第3位(掩码0×08)置位时,表示CRC-32校验位和压缩后大小在File Entry结构的尾部增加一个Data descriptor来记录。
|
偏移 |
字节数 |
描述 |
|
0 |
0/4 |
固定值0x08074b50 |
|
0/4 |
4 |
CRC-32校验 |
|
4/8 |
4 |
压缩后大小 |
|
8/12 |
4 |
压缩前大小 |
1.3. Central Directory
Central Directory File Header
|
偏移 |
字节数 |
描述 |
|
0 |
4 |
固定值0x02014b50 |
|
4 |
2 |
压缩版本 |
|
6 |
2 |
解压缩版本 |
|
8 |
2 |
标志 |
|
10 |
2 |
压缩方式 |
|
12 |
2 |
文件最后修改时间 |
|
14 |
2 |
文件最后修改日期 |
|
16 |
4 |
CRC-32校验 |
|
20 |
4 |
压缩后大小 |
|
24 |
4 |
压缩前大小 |
|
28 |
2 |
文件名称长度(n) |
|
30 |
2 |
扩展字段长度(m) |
|
32 |
2 |
文件注释长度(k) |
|
34 |
2 |
文件开始的分卷号 |
|
36 |
2 |
文件内部属性 |
|
38 |
4 |
文件外部属性 |
|
42 |
4 |
对应文件实体在文件中的偏移 |
|
46 |
n |
文件名称 |
|
46+n |
m |
扩展字段 |
|
46+n+m |
k |
文件注释 |
End of Central Directory record
所有的File Header结束后是该数据结构
|
偏移 |
字节数 |
描述 |
|
0 |
4 |
固定值0x06054b50 |
|
4 |
2 |
当前分卷号 |
|
6 |
2 |
Central Directory的开始分卷号 |
|
8 |
2 |
当前分卷Central Directory的记录数量 |
|
10 |
2 |
Central Directory的总记录数量 |
|
12 |
4 |
Central Directory的大小 (bytes) |
|
16 |
4 |
Central Directory的开始位置偏移 |
|
20 |
2 |
Zip文件注释长度(n) |
|
22 |
n |
Zip文件注释 |
Q1:Central Directory的作用
通过Central Directory可以快速获取ZIP包含的文件列表,而不用逐个扫描文件,虽然Central Directory的内容和文件原来的头文件有冗余,但是当zip文件被追加到其他文件时,就只能通过Central Directory获取ZIP信息,而不能通过扫描文件的方式,因为central directory可能声明一些文件被删除或者已经更新。Central Directory中Entry的顺序可以和文件的实际顺序不一样。
Q2:ZIP如何更新文件
举例说明:一个ZIP包含A、B和C三个文件,现在准备删除文件B,并且对C进行了更新,可以将新的文件C 添加到原来ZIP的后面,同时添加一个新的Central Directory,仅仅包含文件A和新文件C,这样就实现了删除文件B和更新文件C。
在ZIP设计之初,通过软盘来移动文件很常见,但是读写磁盘是很消耗性能的,对于一个很大的ZIP文件,只想更新几个小文件,如果采用这种方式效率非常低。
2,ZIP文件解压
Android提供两种解压ZIP文件的方法:ZipFile和ZipInputStream
2.1 ZipInputStream
ZipInputStream通过流式来顺序访问ZIP,当读到某个文件结尾时(Entry)返回-1,通过getNextEntry来判断是否要继续向下读,ZipInputStream 的read方法的流程图如下。

Q3:为什么要判断是否是压缩文件?
因为文件在添加到ZIP时,可以通过设置Entry.setMethod(ZipEntry.STORED)以非压缩的形式添加到文件,所以在解压时,对于这种情况,可以直接读文件返回,不需要要解压。
这里要重点介绍一下InflaterInputStream.read()方法,其流程图如下。

从流程图可以看出,java层将待解压的数据通过我们定义的Buffer传入native层。每次传入的数据大小是固定值为512字节,在InflaterInputStream.java中定义如下:
static final int BUF_SIZE = 512;
对于压缩文件来说,最终会调用zlib中的inflate.c来解压文件,inflate.c通过状态机来对文件进行解压,将解压后的数据再通过Buffer返回。对inflate解压算法感兴趣的同学可以看源码,传送门http://androidxref.com/4.4.4_r1/xref/external/zlib/src/inflate.c,返回count字节并不等于buffer的大小,取决于inflate解压返回的数据。
2.2 ZipFile
ZipFile通过RandomAccessFile随机访问zip文件,通过Central Directory得到zip中所有的Entry, Entry中包含文件的开始位置和size,前期读Central Directory可能会耗费一些时间,但是后面就可以利用RandomAccessFile的特性,每次读入更多的数据来提高解压效率。
ZipFile中定义了两个类,分别是RAFStream和ZipInflaterInputStream,这两个类分别继承自RandomAccessFile和InflateInputStream,通过getInputStream()返回,ZipFile的解压流程和ZipInputStream类似。
ZipFile和ZipInputStream真正不同的地方在InflaterInputStream.fill(),fill源码如下:
188 protected void fill() throws IOException {
189 checkClosed();
190 if (nativeEndBufSize > 0) {
191 ZipFile.RAFStreamis = (ZipFile.RAFStream) in;
192 len = is.fill(inf, nativeEndBufSize);
193 } else {
194 if ((len = in.read(buf)) > 0) {
195 inf.setInput(buf, 0, len);
196 }
197 }
198 }
下面同样给出InflaterInputStream.read()的流程图,大家就能明白二者的区别之处。

从流程图可以看出,ZipFile的读文件是在native层进行的,每次读文件的大小是由java层传入的,定义如下:
Math.max(1024, (int) Math.min(entry.getSize(), 65535L));
即ZipFile每次处理的数据大小在1KB和64KB之间,如果文件大小介于二者之间,则可以一次将文件处理完。而对于ZipInputStream来说,每次能处理的数据只能是512个字节,所以ZipFile的解压效率更高。
3,ZipFile vs ZipInputStream效率对比
解压文件可以分三步:
1,从磁盘读出zip文件
2,调用inflate解压出数据
3,存储解压后的数据
因此两者的效率对比可以细化到这三个步骤来对比。
3.1 读磁盘
ZipFile在native层读文件,并且每次读的数据在1KB~64KB之间,ZipInputStream只有采用更大的Buffer才可能达到ZipFile的性能。
3.2 infalte解压效率
从上文可知,inflate每次解压的数据是不定的,一方面和inflate的解压算法有关,另一方面取决native层infalte.c每次处理的数据,以上分析可以,ZipInputStream每次只传递512字节数据到native层,而ZipFile每次传递的数据可以在1KB~64KB,所以ZipFile的解压效率更高。从java_util_zip_Inflater.cpp源码看,这是Android做的特别优化。
demo验证(关键代码):
ZipInputStream:
FileInputStream fis =new FileInputStream(files);
ZipInputStream zis =new ZipInputStream(new BufferedInputStream(fis));
byte[] buffer = newbyte[8192];
while((ze=zis.getNextEntry())!=null){
File dstFile = newFile(dir+"/"+ze.getName());
FileOutputStreamfos = new FileOutputStream(dstFile);
while((count = zis.read(buffer)) !=-1){
System.out.println(count);
fos.write(buffer,0,count);
} }
ZipFile关键代码:
ZipFile zipFile = newZipFile(files);
InputStreamis = null;
Enumeratione = zipFile.entries();
while(e.hasMoreElements()) {
entry= (ZipEntry) e.nextElement();
is= zipFile.getInputStream(entry);
dstFile = newFile(dir+"/"+entry.getName());
fos= new FileOutputStream(dstFile);
byte[]buffer = new byte[8192];
while((count = is.read(buffer, 0, buffer.length)) != -1){
fos.write(buffer,0,count);
} }
我们用两个不同压缩率的文件对demo进行测试,文件说明如下。
|
组成 |
压缩前size(MB) |
压缩后size(MB) |
压缩率 |
|
|
低压缩率ZIP |
4个文本文件 |
17 |
1.25 |
7% |
|
高压缩率ZIP |
100个jpg图片 |
9.76 |
9.69 |
99% |
测试数据:
|
文件类型 |
低压缩率文件 |
高压缩率文件 |
||
|
对比指标 |
read调用次数 |
耗时(ms) |
read调用次数 |
耗时(ms) |
|
ZipInputStream |
3588 |
1082.8 |
19900 |
3548.8 |
|
ZipFile |
2181 |
848.4 |
1400 |
971.2 |
|
ZipFile减少百分比 |
39% |
22% |
93% |
73% |
结论:1,ZipFile的read调用的次数减少39%~93%,可以看出ZipFile的解压效率更高
2,ZipFile解压文件耗时,相比ZipInputStream有22%到73%的减少
3.3 存储解压后的数据
从上文可以知道,inflate解压后返回的数据可能会小于buffer的长度,如果每次在read返回后就直接写文件,此时buffer可能并没有充满,造成buffer的利用效率不高,此处可以考虑将解压出的数据输出到BufferedOutputStream,等buffer满后再写入文件,这样做的弊端是,因为要凑满buffer,会导致read的调用次数增加,下面就对ZipFile和Zipinputstream做一个对比。
demo(关键代码):
ZipInputStream:
FileInputStream fis = new FileInputStream(files);
ZipInputStream zis = new ZipInputStream(newBufferedInputStream(fis));
byte[] buffer = new byte[8192];
while((ze=zis.getNextEntry())!=null){
File dstFile = newFile(dir+"/"+ze.getName());
FileOutputStream fos =new FileOutputStream(dstFile);
BufferedOutputStream fos = new BufferedOutputStream(dstFile);
while((count = zis.read(buffer))!= -1){
fos.write(buffer,0,count);
} }
ZipFile:
ZipFile zipFile = new ZipFile(files);
InputStream is = null;
Enumeration e = zipFile.entries();
while (e.hasMoreElements()) {
entry = (ZipEntry)e.nextElement();
is = new BufferedInputStream(zipFile.getInputStream(entry));
dstFile = newFile(dir+"/"+entry.getName());
fos = newFileOutputStream(dstFile);
byte[] buffer = newbyte[8192];
while( (count =is.read(buffer, 0, buffer.length)) != -1){
fos.write(buffer,0,count);
} }
同样对上面的两个压缩文件进行解压,测试数据如下:
|
低压缩率(ms) |
高压缩率(ms) |
|
|
ZipInputStream |
930.2 |
1347.2 |
|
ZipFile |
794.5 |
1056.8 |
|
ZipFile耗时减少 |
15% |
22% |
结论:1,ZipFile较ZipInputStream相比,耗时仍有15%-22%的减少
2,与不使用Buffer相比,ZipInputStream的耗时减少14%-62%,ZipFile解压低压缩率文件耗时有6%的减少,但是对于高压缩率,耗时将有9%的增加(虽然减少了写磁盘的次数,但是为了凑足buffer,增加了read的调用次数,导致整体耗时增加)
Q4:那么问题来了,既然ZipFile效率这么好,那ZipInputStream还有存在的价值吗?
千万别被数据迷惑了双眼,上面的测试仅仅是覆盖了一种场景,即:文件已经在磁盘中存在,且需全部解压出ZIP中的文件,如果你的场景符合以上两点,使用ZipFile无疑是正确无比。同时,也可以利用ZipFile的随机访问能力,实现解压ZIP中间的某几个文件。
但是在以下场景,ZipFile则会略显无力,这是ZipInputStream价值就体现出来了:
1,当文件不在磁盘上,比如从网络接收的数据,想边接收边解压,因ZipInputStream是顺序按流的方式读取文件,这种场景实现起来毫无压力。
2,如果顺序解压ZIP前面的一小部分文件, ZipFile也不是最佳选择,因为ZipFile读CentralDirectory会带来额外的耗时。
3,如果ZIP中CentralDirectory遭到损坏,只能通过ZipInputStream来按顺序解压。
4,结论
1,如果ZIP文件已保存在磁盘,且解压ZIP中的所有文件,建议用ZipFile,效率较ZipInputStream有15%~27%的提升。
2,仅解压ZIP中间的某些文件,建议用ZipFile
3,如果ZIP没有在磁盘上或者顺序解压一小部分文件,又或ZIP文件目录遭到损坏,建议用ZipInputStream
从以上分析和验证可以看出,同一种解压方法使用的方式不同,效率也会相差甚远,最后再回顾一下ZipInputStream和ZipFile最高效的用法(红色为关键部分)。
ZipInputStream:
ZipInputStream zis = new ZipInputStream(newBufferedInputStream(fis));
FileOutputStream fos = new FileOutputStream(dstFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] buffer = new byte[8192];
while((ze=zis.getNextEntry())!=null){
while((count = zis.read(buffer))!= -1){
fos.write(buffer,0,count);
} }
ZipFile:
Enumeration e = ZipFile.entries();
while (e.hasMoreElements()) {
entry = (ZipEntry)e.nextElement();
if 低压缩率文件,如文本
is = new BufferedInputStream(zipFile.getInputStream(entry));
else if高压缩率文件,如图片
is =zipFile.getInputStream(entry);
byte[]buffer = new byte[8192];
while( (count =is.read(buffer, 0, buffer.length)) != -1){
fos.write(buffer,0,count);} }
Android 解压zip文件你知道多少?的更多相关文章
- Android 解压zip文件(支持中文)
过了n多天后,当再次使用原先博客上写的那篇: Android 压缩解压zip文件 去做zip包的解压的时候,出现了原来没有发现的很多问题.首先是中文汉字问题,使用java的zip包不能很好的解决解压问 ...
- Android 解压zip文件
过了n多天后,当再次使用原先博客上写的那篇: Android 压缩解压zip文件 去做zip包的解压的时候,出现了原来没有发现的很多问题.首先是中文汉字问题,使用java的zip包不能很好的解决解压问 ...
- 通过javascript在网页端解压zip文件并查看压缩包内容
WEB前端解压ZIP压缩包 web前端解压zip文件有什么用: 只考虑标准浏览器的话, 服务器只要传输压缩包到客户端, 节约了带宽, 而且节约了传输时间, 听起来好像很厉害的说: 如果前端的代 ...
- (转载)C#压缩解压zip 文件
转载之: C#压缩解压zip 文件 - 大气象 - 博客园http://www.cnblogs.com/greatverve/archive/2011/12/27/csharp-zip.html C# ...
- java实现解压zip文件,(亲测可用)!!!!!!
项目结构: Util.java内容: package com.cfets.demo; import java.io.File; import java.io.FileOutputStream; imp ...
- python用zipfile模块打包文件或是目录、解压zip文件实例
#!/usr/bin/env python # -*- coding: utf-8 -*- from zipfile import * import zipfile #解压zip文件 def unzi ...
- AIX解压ZIP文件
AIX系统自身是没有解压ZIP文件的,但在AIX安装oracle数据库服务器的话,在$ORACLE_HOME/bin路径下方却有unzip命令,可以解压ZIP文件. 一.shell脚本 之前的版本 ...
- linux 解压zip文件
linux 解压zip文件 学习了:https://blog.csdn.net/hbcui1984/article/details/1583796 unzip xx.zip
- Java 上传解压zip文件,并且解析文件里面的excel和图片
需求:上传一个zip文件,zip文件里面包含一个excel和很多图片,需要把excel里面的信息解析出来保存到表中,同时图片也转化成base64保存到数据库表中. PS:为了方便不同水平的开发人员阅读 ...
随机推荐
- 关于用舞蹈链DLX算法求解数独的解析
欢迎访问——该文出处-博客园-zhouzhendong 去博客园看该文章--传送门 描述 在做DLX算法题中,经常会做到数独类型的题目,那么,如何求解数独类型的题目?其实,学了数独的构建方法,那么DL ...
- 009 spring boot中文件的上传与下载
一:任务 1.任务 文件的上传 文件的下载 二:文件的上传 1.新建一个对象 FileInfo.java package com.cao.dto; public class FileInfo { pr ...
- 最短路(Floyd)-hdu1317
题目链接:https://vjudge.net/problem/HDU-1317 题目描述: 题意:玩家起始有100个能量点,刚开始在起始房间中,每个房间外有一条单向的路径通往其他房间(一个房间可能通 ...
- MongDB-基础
首先吐槽一下,MongDB用到了JS的引擎,只要涉及到了JS,语法就变得又臭又长,真是无语 还有,MongDB的安装真是麻烦,我用的是win10环境,怎么装都报服务错误,redis一装就可以用,希望m ...
- 深入理解JS防抖与节流
参考博客:JS防抖和节流,感谢作者的用心分享 日常开发过程中,滚动事件做复杂计算频繁调用回调函数很可能会造成页面的卡顿,这时候我们更希望把多次计算合并成一次,只操作一个精确点,JS把这种方式称为deb ...
- python数据结构之希尔排序
def shell_sort(alist): n=len(alist) gap= int(n / 2) #步长 while gap>0: for i in range(gap,n): j=i w ...
- 自己总结的C#编码规范--1.命名约定篇
命名约定 我们在命名标识符时(包括参数,常量,变量),应使用单词的首字母大小写来区分一个标识符中的多个单词,如UserName. PascalCasing PascalCasing包含一到多个单词,每 ...
- iOS中 H5的input输入框focus()无法自动拉起键盘(解决方法)
ios的hybird APP 无法使用focus()获取焦点和键盘的问题. 解决方案 原来,在App的配置文件(config.xml),里面默认会有一句 1 <preference name=& ...
- Spring AOP 配置通知方法的时候如何处理方法重载
如何在method属性里指定重载方法中的某一个?
- POJ 水题(刷题)进阶
转载请注明出处:優YoU http://blog.csdn.net/lyy289065406/article/details/6642573 部分解题报告添加新内容,除了原有的"大致题意&q ...
