转自: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
1.2. Data descriptor
当头部标志第3位(掩码0×08)置位时,表示CRC-32校验位和压缩后大小在File Entry结构的尾部增加一个Data descriptor来记录。
1.3. Central Directory
Central Directory File Header
End of Central Directory record
所有的File Header结束后是该数据结构
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源码如下:
protected void fill() throws IOException {
checkClosed();
if (nativeEndBufSize > 0) {
ZipFile.RAFStreamis = (ZipFile.RAFStream) in;
len = is.fill(inf, nativeEndBufSize);
} else {
if ((len = in.read(buf)) > 0) {
inf.setInput(buf, 0, len);
}
}
}
下面同样给出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进行测试,文件说明如下。
测试数据:
结论: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);
}
}
同样对上面的两个压缩文件进行解压,测试数据如下:
结论: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);
}
}