<技术[转]>Android 解压zip文件

转自:Android 解压zip文件你知道多少?

  • 对于Android 常用的压缩格式ZIP ,你了解多少?

  • Android 的有两种解压ZIP 的方法,你知道吗?

  • ZipFile 和ZipInputStream 的解压效率,你对比过吗?

带着以上问题,现在就开始ZIP的解压之旅。

1. Zip文件结构

ZIP文件结构如下图所示, File Entry表示一个文件实体,一个压缩文件中有多个文件实体。

文件实体由一个头部和文件数据组,Central Directory由多个File header组成,每个File header都保存一个文件实体的偏移,文件最后由End of central directory结束。

image

1.1 Local File Header


image.png

1.2. Data descriptor
当头部标志第3位(掩码0×08)置位时,表示CRC-32校验位和压缩后大小在File Entry结构的尾部增加一个Data descriptor来记录。


image.png

1.3. Central Directory
Central Directory File Header

image.png

End of Central Directory record
所有的File Header结束后是该数据结构


image.png

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方法的流程图如下。

image.png

Q3****:为什么要判断是否是压缩文件?

因为文件在添加到ZIP时,可以通过设置Entry.setMethod(ZipEntry.STORED)以非压缩的形式添加到文件,所以在解压时,对于这种情况,可以直接读文件返回,不需要要解压。

这里要重点介绍一下InflaterInputStream.read()方法,其流程图如下。

image

从流程图可以看出,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()的流程图,大家就能明白二者的区别之处。


image

从流程图可以看出,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);
    }
 }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容