• Android 解压zip文件你知道多少?


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

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

    • ZipFileZipInputStream的解压效率,你对比过吗?

    带着以上问题,现在就开始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文件注释

    Q1Central Directory的作用

    通过Central Directory可以快速获取ZIP包含的文件列表,而不用逐个扫描文件,虽然Central Directory的内容和文件原来的头文件有冗余,但是当zip文件被追加到其他文件时,就只能通过Central Directory获取ZIP信息,而不能通过扫描文件的方式,因为central directory可能声明一些文件被删除或者已经更新。Central DirectoryEntry的顺序可以和文件的实际顺序不一样。

    Q2ZIP如何更新文件

    举例说明:一个ZIP包含ABC三个文件,现在准备删除文件B,并且对C进行了更新,可以将新的文件添加到原来ZIP的后面,同时添加一个新的Central Directory,仅仅包含文件A和新文件C,这样就实现了删除文件B和更新文件C

    ZIP设计之初,通过软盘来移动文件很常见,但是读写磁盘是很消耗性能的,对于一个很大的ZIP文件,只想更新几个小文件,如果采用这种方式效率非常低。

    2ZIP文件解压

    Android提供两种解压ZIP文件的方法:ZipFileZipInputStream

    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中定义了两个类,分别是RAFStreamZipInflaterInputStream,这两个类分别继承自RandomAccessFileInflateInputStream,通过getInputStream()返回,ZipFile的解压流程和ZipInputStream类似。

    ZipFileZipInputStream真正不同的地方在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, (intMath.min(entry.getSize(), 65535L));

    ZipFile每次处理的数据大小在1KB64KB之间,如果文件大小介于二者之间,则可以一次将文件处理完。而对于ZipInputStream来说,每次能处理的数据只能是512个字节,所以ZipFile的解压效率更高。

    3ZipFile vs ZipInputStream效率对比

    解压文件可以分三步:

    1,从磁盘读出zip文件

    2,调用inflate解压出数据

    3,存储解压后的数据

    因此两者的效率对比可以细化到这三个步骤来对比。

    3.1 读磁盘

    ZipFilenative层读文件,并且每次读的数据在1KB~64KB之间,ZipInputStream只有采用更大的Buffer才可能达到ZipFile的性能。

    3.2 infalte解压效率

    从上文可知,inflate每次解压的数据是不定的,一方面和inflate的解压算法有关,另一方面取决nativeinfalte.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进行测试,文件说明如下。


    组成

    压缩前sizeMB

    压缩后sizeMB

    压缩率

    低压缩率ZIP

    4个文本文件

    17

    1.25

    7%

    高压缩率ZIP

    100jpg图片

    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%

    结论:1ZipFileread调用的次数减少39%~93%,可以看出ZipFile的解压效率更高

    2ZipFile解压文件耗时,相比ZipInputStream22%73%的减少

    3.3 存储解压后的数据

    从上文可以知道,inflate解压后返回的数据可能会小于buffer的长度,如果每次在read返回后就直接写文件,此时buffer可能并没有充满,造成buffer的利用效率不高,此处可以考虑将解压出的数据输出到BufferedOutputStream,等buffer满后再写入文件,这样做的弊端是,因为要凑满buffer,会导致read的调用次数增加,下面就对ZipFileZipinputstream做一个对比。

    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%

    结论:1ZipFileZipInputStream相比,耗时仍有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也不是最佳选择,因为ZipFileCentralDirectory会带来额外的耗时。

    3,如果ZIPCentralDirectory遭到损坏,只能通过ZipInputStream来按顺序解压。

    4,结论

    1,如果ZIP文件已保存在磁盘,且解压ZIP中的所有文件,建议用ZipFile,效率较ZipInputStream15%~27%的提升。

    2,仅解压ZIP中间的某些文件,建议用ZipFile

    3,如果ZIP没有在磁盘上或者顺序解压一小部分文件,又或ZIP文件目录遭到损坏,建议用ZipInputStream

    从以上分析和验证可以看出,同一种解压方法使用的方式不同,效率也会相差甚远,最后再回顾一下ZipInputStreamZipFile最高效的用法(红色为关键部分)。

    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); 






  • 相关阅读:
    windows10家庭版安装docker踩坑解决记录
    sequelize Getters, Setters & Virtuals
    sequelize模型增删改查询(基础)Model Querying
    sequelize数据库模型关联文档详解
    Nginx配置实现下载文件
    Linux常用命令记录
    nodejs后台如何请求第三方接口request-promise简介及其废弃后的替代方案推荐got、axios
    新版本chrome浏览器(80版本以后)带来的跨域请求cookie丢失问题
    谷歌浏览器 Cookie 的 SameSite 属性 (转)
    JavaScript Image对象 整理
  • 原文地址:https://www.cnblogs.com/jeffen/p/6933257.html
Copyright © 2020-2023  润新知