ZIP文件格式及解压方式分析

什么是ZIP文件

ZIP文件格式是一种数据压缩和文档储存的文件格式,原名Deflate,它的MIME格式为application/zip。很多操作系统天然支持打开ZIP格式的文件,例如,Microsoft从Windows ME操作系统开始就内置对ZIP格式的支持,OS X和Linux操作系统也对zip格式提供了类似的支持。所以,很多时候,即使电脑上没有安装解压缩软件,也能打开,提取和创建ZIP文件。android sdk也内置了ZipFile和ZipInputStream来操作ZIP文件。

ZIP文件格式

ZIP文件大致由三部分组成:数据存储区(File Entry),中央目录区(Central Directory)和目录结束标识(End of central directory record)。

zip文件.drawio (1).png

数据存储区

这是ZIP文件的核心部分,包含了被压缩的源文件数据及文件对应的元数据信息,每一个被压缩的文件在数据存储区都有一个对应的本地文件记录,这个记录详细描述了压缩前后的文件元数据信息。本地文件记录分为三部分:本地文件头部(local file header),文件数据(file data)和数据描述符(data descriptor)

1. 本地文件头(local file header )

本地文件头中包含着文件的各种元数据,例如,文件名称、解压缩版本、压缩方式、CRC冗余校验码等信息。它以固定值0x04034b50作为起始标识。本地文件头的结构如下。

local file header signature     4 bytes  (0x04034b50)
version needed to extract       2 bytes
general purpose bit flag        2 bytes
compression method              2 bytes
last mod file time              2 bytes
last mod file date              2 bytes
crc-32                          4 bytes
compressed size                 4 bytes
uncompressed size               4 bytes
file name length                2 bytes
extra field length              2 bytes

file name (variable size)
extra field (variable size)
  • loca file header signature:文件头标识,一般为固定值0x04034b50

  • version needed to extract:解压该文件所需的最低支持的ZIP规范版本。该字段值=解压所需的最低ZIP规范版本*10,比如,最低支持的ZIP规范版本是2.0,那么该字段的值就是20。每个版本定义如下。

    1.0 - 默认值
    1.1 - 文件是卷标
    2.0 - 文件是一个文件夹(目录)
    2.0 - 使用 Deflate 压缩来压缩文件
    2.0 - 使用传统的 PKWARE 加密对文件进行加密
    2.1 - 使用 Deflate64™ 压缩文件
    2.5 - 使用 PKWARE DCL Implode 压缩文件
    2.7 - 文件是补丁数据集
    4.5 - 文件使用 ZIP64 格式扩展
    4.6 - 使用 BZIP2 压缩文件压缩
    5.0 - 文件使用 DES 加密
    5.0 - 文件使用 3DES 加密
    5.0 - 使用原始 RC2 加密对文件进行加密
    5.0 - 使用 RC4 加密对文件进行加密
    5.1 - 文件使用 AES 加密进行加密
    5.1 - 使用更正的 RC2 加密对文件进行加密
    5.2 - 使用更正的 RC2-64 加密对文件进行加密
    6.1 - 使用非 OAEP 密钥包装对文件进行加密
    6.2 - 中央目录加密genaral purpose bit flag
    
  • genaral purpose bit flag:通用标识位。标识一些通用信息,其中部分bit的含义如下(完整示意请参考zip文档官方说明)。

    • bit0:如果为1,表示文件被加密。
    • bit3:如果为1,那么crc32,compressed size 和 uncompressed size这些字段在local file header中将会被设置为0,真正的值是放在数据描述符(data descriptor)中。
  • compression method:压缩方式,支持的压缩方式如下。

    0 - The file is stored (no compression)
    1 - The file is Shrunk
    2 - The file is Reduced with compression factor 1
    3 - The file is Reduced with compression factor 2
    4 - The file is Reduced with compression factor 3
    5 - The file is Reduced with compression factor 4
    6 - The file is Imploded
    7 - Reserved for Tokenizing compression algorithm
    8 - The file is Deflated
    9 - Enhanced Deflating using Deflate64(tm)
    10 - PKWARE Data Compression Library Imploding
    11 - Reserved by PKWARE
    12 - File is compressed using BZIP2 algorithm
    

    常用的压缩方式是Deflated方式,android中的apk和默认的ZIP包都是Deflated压缩方式。

  • last mod file time:最后修改文件的时间。长度为2个byte,每个bit含义如下。

    bit 含义
    0-4 秒除以2的值
    5-10 分钟(0-59)
    11-15 小时(0-23)
  • last mod date time:最后修改文件的日期。长度为2byte,每个bit含义如下。

    bit 含义
    0-4 日(1-31)
    5-10 月(1-12)
    11-15 年,当前年份减去1980的差值
  • crc-32:使用crc-32算法计算的冗余校验码。

  • compressed size:压缩后文件大小,长度4个字节,单位为byte,由此可以推断出标准ZIP格式最大压缩容量为:2^32 - 1 bytes ≈ 4 GB,而ZIP64格式最大压缩容量为 2^64 - 1 bytes ≈ 16EB。

  • uncompressed size:未压缩文件的大小,单位为byte,长度为4个字节。

  • file name length:文件名长度。

  • extra field length:文件扩展区域数据长度。

  • file name:文件名。

  • extral field:扩展区数据。

2. 文件数据(file data)

紧跟在本地文件头之后就是文件数据区,它存储的是被压缩后的文件数据,也是要解压的对象。

3. 数据描述符(data descriptor)

该区域只有在文件头的genaral purpose bit flag的第3位(0x0008)为1时才存在,紧跟在压缩文件的数据区之后,在磁盘上的ZIP文件一般没有数据描述符。

中央目录区(Central directory)

中央目录区通常由多个文件头(file header)组成,每一个被压缩的文件都有一个对应的file header(注意,这里不是local file header),用于标识和定位该文件在ZIP文件中的位置。这个文件头和本地文件头类似,记录了被压缩文件的元数据信息,包括文件原始大小,压缩之后的大小,文件注释等。

中心目录区的结构如下。

[file header 1]
.
.
. 
[file header n]
[digital signature]
1. 文件头(file header)

中央目录区的文件头中记录的文件元数据和本地文件头中的数据十分类似,有很多字段都是相同的,但它比本地文件头多了一些信息。

中央目录区文件头结构如下。

central file header signature   4 bytes  (0x02014b50)
version made by                 2 bytes
version needed to extract       2 bytes
general purpose bit flag        2 bytes
compression method              2 bytes
last mod file time              2 bytes
last mod file date              2 bytes
crc-32                          4 bytes
compressed size                 4 bytes
uncompressed size               4 bytes
file name length                2 bytes
extra field length              2 bytes
file comment length             2 bytes
disk number start               2 bytes
internal file attributes        2 bytes
external file attributes        4 bytes
relative offset of local header 4 bytes

file name (variable size)
extra field (variable size)
file comment (variable size)

下面对中央目录区中文件头特有的字段进行说明,其他字段可参考本地文件头对应的字段说明。

  • central file header signature:中央目录头文件起始标识,为固定数值0x02014b50

  • version made by:压缩所使用的pkware版本。

  • file comment length:该文件注释长度,每个文件都可以添加注释。

  • disk number start:文件开始的分卷号。

  • relative offset of local header:相对于本地文件头的偏移,通过这个可以找到本地文件头,进而找到对应的文件数据(file data)。

  • file comment:文件注释。

2. 数据签名(digital signature)
header signature                4 bytes  (0x05054b50)
size of data                    2 bytes
signature data (variable size)
  • header signature:数字签名起始标识,固定值为0x05054b50
  • size of data:数字签名数据大小。
  • signature data :签名数据

中央目录结束标识(end of central directory record)

中央目录结束标识的主要作用是用来定位中央目录记录区的开始位置,同时记录整个ZIP文件的注释内容。中央目录结束标识的结构如下。

end of central dir signature    4 bytes  (0x06054b50)
number of this disk             2 bytes
number of the disk with the
start of the central directory  2 bytes
total number of entries in the
central directory on this disk  2 bytes
total number of entries in
the central directory           2 bytes
size of the central directory   4 bytes
offset of start of central
directory with respect to
the starting disk number        4 bytes
.ZIP file comment length        2 bytes
.ZIP file comment       (variable size)
  • end of central dir signature:中央目录结束标识 ,固定值0x06054b50。
  • number of this disk:当前磁盘编号。
  • number of the disk with the start of the central directory:中央目录开始位置的磁盘编号。
  • total number of entries in the central directory on this disk:该磁盘所记录的中央目录entry数量。
  • total number of entries in the central directory:中央目录中总共的entry数量。
  • size of the central directory:中央目录区大小。
  • offset of start of central directory with respect to the starting disk number:中央目录区开始位置偏移。
  • ZIP file comment length:zip文件注释长度。
  • ZIP file comment:zip文件注释。

中央目录结束标识是ZIP文件解压的入口。通过读取中央目录结束标识,解压缩软件可以快速地找到中央目录,并据此解析整个ZIP文件的结构和内容。通过里面的中央核心目录区的大小可以找到对应的中央目录模块,然后根据中央目录文件头中的本地文件头偏移(relative offset of local header)可以寻址到对应的文件,并进行解压。

每个压缩文件都必须且仅有一个中央目录结束标识。如果ZIP文件损坏或结构不正确,可能会导致中央目录结束标识丢失或损坏,从而使得解压缩软件无法正确读取和解析ZIP文件。

ZIP文件解压流程

方式1 通过解析中央目录区来解压

通过ZIP文件的结构我们发现,ZIP文件的中央目录区保存了所有的文件信息。所以,可以通过中央目录区拿到所有的文件信息并进行解压,步骤如下所示。

解压流程.drawio.png
  • 首先在 ZIP 文件末尾通过中央目录结束标识 (0x06054b50)找到中央目录结束标识数据块。
  • 通过中央目录结束标识中的中央目录区开始位置偏移找到中央目录区数据块。
  • 根据中央目录区的File Header中的 local file header的偏移量找到对应的local file header。
  • 根据 local file header找到对应的file data
  • 解密 file data(如果需要);
  • 解压 file data;

方式2 通过读取本地文件头来解压

根据 ZIP 文件格式标准可知,除了 中央目录区, 本地文件头中也包含了每个文件的相关信息。因此,可以基于本地文件头去解压文件数据,其解压流程就可以变为:

  • 从头开始,通过本地文件头标识搜索对应的 local file header
  • 读取 local file header并找到file data
  • 解密 file data(如果需要);
  • 解压 file data;

两种解压方式对比

通过两种解压方式可以明显看出,两种解压方式适用的场景不同。

方式1适用场景

  • 适用于在解压文件已经存在于磁盘上,并且需要解压压缩包中所有的文件。

方式2适用场景

  • 当文件不在磁盘上,比如从网络接收的数据,想边接收边解压;

  • 需要顺序解压ZIP文件前面的一小部分文件,可以使用这种方式,因为方式1读中央目录区会带来额外的耗时;

  • ZIP文件中的中央目录区遭到损坏;

Android中两种解压方式

针对ZIP文件解压,android中提供两种解压方式,即ZipFile和ZipInputStream。

解压一个ZIP文件,其实大致分为三个步骤。

  1. 从磁盘读出ZIP文件
  2. 调用解压算法解压出数据
  3. 存储解压后的数据

下面我们可以从上面的步骤来分析Android中的两种解压方式。

ZipInputStream解压方式

使用ZipInputStream解压文件的关键代码如下。

FileInputStream fis =new FileInputStream(zipfile);
ZipInputStream zis =new ZipInputStream(fis);
while((ze=zis.getNextEntry())!=null){
  File dstFile = newFile(dir+"/"+ze.getName());
  FileOutputStream fileOutputStream = new FileOutputStream(dstFile);
  BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
  int n;
  byte[] buffer = new byte[8192];
  while ((n = zis.read(buffer)) != -1) {
      bufferedOutputStream.write(buffer, 0, n);
  }
}

这段代码的关键是zis.getNextEntry()和zis.read()这两个方法,下面分析下这两个方法。

ZipInputStream#getNextEntry()
public ZipEntry getNextEntry() throws IOException {
    ......
    if ((entry = readLOC()) == null) {
        return null;
    }
    ......
    return entry;
}

省略掉不太重要的代码,发现getNextEntry()方法最终是通过readLOC()方法来构建了一个entry。继续看readLOC()方法。

ZipInputStream#readLOC()

private ZipEntry readLOC() throws IOException {
    try {
        //--->注释1
        readFully(tmpbuf, 0, LOCHDR);
    } catch (EOFException e) {
        return null;
    }
  
    //--->注释2
    if (get32(tmpbuf, 0) != LOCSIG) {
        return null;
    }
    // get flag first, we need check USE_UTF8.
    flag = get16(tmpbuf, LOCFLG);
    // get the entry name and create the ZipEntry first
    int len = get16(tmpbuf, LOCNAM);
    int blen = b.length;
    if (len > blen) {
        do {
            blen = blen * 2;
        } while (len > blen);
        b = new byte[blen];
    }
    readFully(b, 0, len);
    // Force to use UTF-8 if the USE_UTF8 bit is ON
    ZipEntry e = createZipEntry(((flag & USE_UTF8) != 0)
                                ? zc.toStringUTF8(b, len)
                                : zc.toString(b, len));
    // now get the remaining fields for the entry
    if ((flag & 1) == 1) {
        throw new ZipException("encrypted ZIP entry not supported");
    }
    e.method = get16(tmpbuf, LOCHOW);
    e.xdostime = get32(tmpbuf, LOCTIM);
    if ((flag & 8) == 8) {
    } else {
        e.crc = get32(tmpbuf, LOCCRC);
        e.csize = get32(tmpbuf, LOCSIZ);
        e.size = get32(tmpbuf, LOCLEN);
    }
    len = get16(tmpbuf, LOCEXT);
    if (len > 0) {
        byte[] extra = new byte[len];
        readFully(extra, 0, len);
        e.setExtra0(extra,
                    e.csize == ZIP64_MAGICVAL || e.size == ZIP64_MAGICVAL);
    }
    return e;
}

其实这个redLOC()方法,第一眼看上去像是read local file header的意思,下面来分析下。

  1. 注释1: 这里ZipConstants.LOCHDR = 30,这里就是去对应的流中读取30个字节,为什么是30个字节,我们可以再回头看下zip文件中local file header的结构,发现local file header前面固定信息的长度,也就是到extra field lenght字段的长度正好是30个byte。这个方法就是要解析local file header,因此这里把前30个byte读到了tmp这个内存中。
  2. 注释2: 这里ZipConstants.LOCSIG=0x04034b50L,这个很熟悉吧,其实就是local file header的起始标识,get32表示从tmp中读取4个字节,而local file header的前四个字节就是起始标识,所以这里判断,如果这个值不等于0x04034b50,表示不是标准的local file header,所以会返回null。
  3. 后面的代码很简单,其实就是在解析local file header中的各个字段。解析完local file header的各个字段后,构建一个entry对象返回。

所以,ZipInputStream的getNextEntry()方法构建出来的其实就是代码local file header的对象。从这里也可以看出,ZipInputStream解压ZIP文件是从local file header开始的,也就是说并没有去读central directory,这种属于上面提到的第二种解压方式。

ZipInputStream#read()
public int read(byte[] b, int off, int len) throws IOException {
        ......
    switch (entry.method) {
    case DEFLATED:
        len = super.read(b, off, len);
                ......
        return len;
    case STORED:
            ......
        len = in.read(b, off, len);
                ......
        return len;
    default:
        throw new ZipException("invalid compression method");
    }
}

在上面代码中,通过判断entry.method(local file header中的 compression method字段)分为两种情况,DEFLATED表示压缩格式,STORED表示没有压缩。

  • entry.method = DEFLATED时,调用super.read(b, off, len)
  • entry.method = STORED时,调用in.read(b, off, len)

通过代码分析,我们看到ZipInputStream继承自InflaterInputStream,而in是构建ZipInputStream时传入构造方法的输入流,我们在代码示例中传入的是FileInputStream,所以上面两种情况等同于。

  • entry.method = DEFLATED时,调用InflaterInputStream.read(b, off, len)
  • entry.method = STORED时,调用FileInputStream.read(b, off, len)

其实很好理解,InflaterInputStream是一个带有解压功能的输入流,而FileInputStream是一个普通的输入流,如果文件压根没有压缩,那么就直接调用普通的流读文件就行,如果文件压缩了,调用InflaterInputStream来进行解压。

下面,我们来看下InflaterInputStream.read(b, off, len)

InflaterInputStream#read(b, off, len)

public int read(byte[] b, int off, int len) throws IOException {
        ......
    int n;
    //--->注释1
    while ((n = inf.inflate(b, off, len)) == 0) {
        ......
        //--->注释2  
        if (inf.needsInput()) {
          
                //--->注释3
            fill();
        }
    }
    return n;
}
  1. 注释1,这里通过调用inf.inflate方法来解压数据并将解压后的数据放入缓存b中,这里的inf变量是InflaterInputStream的成员变量,它是在InflaterInputStream的构造方法中赋值的。

    InflaterInputStream构造方法

    public InflaterInputStream(InputStream in, Inflater inf, int size) {
        super(in);
            ......
        this.inf = inf;
        buf = new byte[size];
    }
    

    而ZipInputStream继承自InflaterInputStream,在ZipInputStream的构造方法中调用了super方法,其实就是InflaterInputStream的构造方法。下面看下ZipInputStream的构造方法。

    ZipInputStream构造方法

    public ZipInputStream(InputStream in, Charset charset) {
        super(new PushbackInputStream(in, 512), new Inflater(true), 512);
            ......
    }
    

    (1) 通过ZipInputStream的构造方法,可以看到

    (2) InflaterInputStream.in是PushbackInputStream对象;

    (3) InflaterInputStream.inf是一个Inflater对象,

    (4) InflaterInputStream.buf的size是512

    (5) 而PushbackInputStream其实是对输入流in(在上面的例子中是FileInputStream)的包装类。

  2. 注释2,这里判断是否有足够的数据可以让inf来进行解压,如果没有,那么就调用fill方法来填充数据。

  3. 注释3,这里调用fill方法来将压缩数据从磁盘读到内存中,给inflate方法解压。

从read()方法看到,InflaterInputStream这个输入流其实就是用来从硬盘读取压缩数据到内存并进行解压的一个输入流。解压数据流程如下:

  • 通过needsInput()方判断当前是否有待解压的数据
  • 如果没有待解压数据,就调用fill()方法来读取压缩前的数据到内存中。
  • 如果有待解压数据,就调用inflate()方法来解压数据到内存中。

Inflater#inflate

/*
Params:
    b – the buffer for the uncompressed data 
    off – the start offset of the data 
    len – the maximum number of uncompressed bytes
Returns: the actual number of uncompressed bytes
*/
public int inflate(byte[] b, int off, int len){
        ......
    synchronized (zsRef) {
        ensureOpen();
        int thisLen = this.len;
        int n = inflateBytes(zsRef.address(), b, off, len);
        bytesWritten += n;
        bytesRead += (thisLen - this.len);
        return n;
    }
}

private native int inflateBytes(long addr, byte[] b, int off, int len);

这里通过调用native层的inflateBytes方法,将压缩后的数据进行解压,解压后的数据存储在来数组b中,同时会返回实际解压的字节数。

Inflater#needInput

/**
 * Returns true if no data remains in the input buffer. This can
 * be used to determine if #setInput should be called in order
 * to provide more input.
 * @return true if no data remains in the input buffer
 */
public boolean needsInput() {
    synchronized (zsRef) {
        return len <= 0;
    }
}

这个方法用于检查输入的buf中是否还有待解压的数据,如果没有,就需要调用setInput()方法来进行填充。

InflaterInputStream#fill

/**
 * Fills input buffer with more data to decompress.
 */
protected void fill() throws IOException {
    //--->注释1
    len = in.read(buf, 0, buf.length);
    ......
    //--->注释2  
    inf.setInput(buf, 0, len);
}
  1. 注释1,调用输入流in的read方法从磁盘中读取未解压的数据到buf中,读取的最大长度为buf.length。
  2. 注释2,将读取到的未解压的数据放入Inflater的数组buf中,供Inflater进行解压。

通过上面分析InflaterInputStream的构造方法可知,这里buf=new byte[512],即buf.lenght=512,所以通过ZipInputStream进行解压时,每次最多从磁盘(或者网络中)读取512个字节。

in.read(buf, 0, buf.length)其实就是调用PushbackInputStream的read方法,这里就不继续分析了,其实这里就是装饰器模式。

总结

使用ZipInputStream来解压,其实就是通过读取loca file header进行顺序解压的过程,它首先通过ZipInputStream#getNextEntry()方法读取每个文件对应的local file header,然后再使用InflaterInputStream#read(b, off, len)方法对实体文件解压。

ZipFile解压方式

使用ZipFile解压文件的关键代码如下。

ZipFile zipFile = newZipFile(files);
InputStreamis = null;
Enumeration e = 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);
  } 
}

这里有几个关键的方法。

  • ZipFile#entries 用来获取所有的entry,通过查看ZipEntry类的定义(ZipEntry中有comment属性,而注释字段是在中央目录区的file header才存在),可以看到这里的ZipEntry其实代表的是中央目录区的file header,而前面提到的的ZipFileInputStream #getNextEntry其实是代表的是local file header。
  • ZipEntry.hasMoreElements:判断是否有下一个待解压的文件。
  • ZipFile#getInputStream:得到读取解压文件的输入流。
  • is.read:通过流文件读取数据。
ZipFile#entries
public Enumeration<? extends ZipEntry> entries() {
    //---> 注释1
    return new ZipEntryIterator();
}

private class ZipEntryIterator implements Enumeration<ZipEntry>, Iterator<ZipEntry> {
    private int i = 0;

    public ZipEntryIterator() {
        ensureOpen();
    }

    public boolean hasMoreElements() {
        return hasNext();
    }

    public boolean hasNext() {
        synchronized (ZipFile.this) {
            .....
            //--->注释2  
            return i < total;
        }
    }

    public ZipEntry nextElement() {
        return next();
    }

    public ZipEntry next() {
        synchronized (ZipFile.this) {
                        ......
            //--->注释3  
            long jzentry = getNextEntry(jzfile, i++);
                        ......
            //---> 注释4   
            ZipEntry ze = getZipEntry(null, jzentry);
            ......
            return ze;
        }
    }
}
  1. 注释1,这里返回的是一个迭代器ZipEntryIterator,通过这个迭代器,后面通过这个迭代器,可以遍历所有的zip文件。
  2. 注释2,这里的total表示总的文件的数量,这个方法用于判断后面是否还有未解压的文件。
  3. 注释3,这里通过getNextEntry()方法获取解压文件对应的entry,点击去发现getNextEntry是一个native方法,它返回的也不是一个真正的对象,而是一个long型的数据,通过后面分析C代码可以发现,这里返回的其实是一个地址
private static native long getNextEntry(long jzfile, int i);
  1. 注释4,构建ZipEntry对象,这里构建的是Java对象。

ZipFile#getZipEntry

private ZipEntry getZipEntry(String name, long jzentry) {
    ZipEntry e = new ZipEntry();
    e.flag = getEntryFlag(jzentry);  // get the flag first
    if (name != null) {
        e.name = name;
    } else {
            .......
    }
    e.xdostime = getEntryTime(jzentry);
    e.crc = getEntryCrc(jzentry);
    e.size = getEntrySize(jzentry);
    e.csize = getEntryCSize(jzentry);
    e.method = getEntryMethod(jzentry);
    e.setExtra0(getEntryBytes(jzentry, JZENTRY_EXTRA), false);
    byte[] bcomm = getEntryBytes(jzentry, JZENTRY_COMMENT);
    if (bcomm == null) {
        e.comment = null;
    } else {
        if (!zc.isUTF8() && (e.flag & USE_UTF8) != 0) {
            e.comment = zc.toStringUTF8(bcomm, bcomm.length);
        } else {
            e.comment = zc.toString(bcomm, bcomm.length);
        }
    }
    return e;
}

在getZipEntry方法中一些列的get方法(例如getEntryTime,getEntryCrc等)都是native实现的。

从上面分析可以看到,ZipFile读压缩文件的信息都是通过native层来读取的,下面分析一下ZipFile对应的C层代码,ZipFile对应的C代码主要位于下面两个类中。

/libcore/ojluni/src/main/native/ZipFile.c
/libcore/ojluni/src/main/native/zip_util.c  

ZipFile.c文件中,定义了所有JNI调用的方法。

static JNINativeMethod gMethods[] = {
  NATIVE_METHOD(ZipFile, getFileDescriptor, "(J)I"),
  NATIVE_METHOD(ZipFile, getEntry, "(J[BZ)J"),
  NATIVE_METHOD(ZipFile, freeEntry, "(JJ)V"),
  NATIVE_METHOD(ZipFile, getNextEntry, "(JI)J"),
  NATIVE_METHOD(ZipFile, close, "(J)V"),
  NATIVE_METHOD(ZipFile, open, "(Ljava/lang/String;IJZ)J"),
  NATIVE_METHOD(ZipFile, getTotal, "(J)I"),
  NATIVE_METHOD(ZipFile, startsWithLOC, "(J)Z"),
  NATIVE_METHOD(ZipFile, read, "(JJJ[BII)I"),
  NATIVE_METHOD(ZipFile, getEntryTime, "(J)J"),
  NATIVE_METHOD(ZipFile, getEntryCrc, "(J)J"),
  NATIVE_METHOD(ZipFile, getEntryCSize, "(J)J"),
  NATIVE_METHOD(ZipFile, getEntrySize, "(J)J"),
  NATIVE_METHOD(ZipFile, getEntryMethod, "(J)I"),
  NATIVE_METHOD(ZipFile, getEntryFlag, "(J)I"),
  NATIVE_METHOD(ZipFile, getCommentBytes, "(J)[B"),
  NATIVE_METHOD(ZipFile, getEntryBytes, "(JI)[B"),
  NATIVE_METHOD(ZipFile, getZipMessage, "(J)Ljava/lang/String;"),
};

在ZipFile#getNextEntry方法中,需要传入一个jzfile,这个jzfile是C层返回的一个地址,对应的压缩文件在C层的对象。通过代码发现,jzfile是在ZipFile的构造方法中赋值的。

public ZipFile(File file, int mode, Charset charset) throws IOException
{   
    ......
    jzfile = open(name, mode, file.lastModified(), usemmap);
    
    this.name = name;
    this.total = getTotal(jzfile);
    this.locsig = startsWithLOC(jzfile);
    ......
}

在ZipFile.c中找到open方法。

ZipFile.c#open

ZipFile_open(JNIEnv *env, jobject thiz, jstring name,
                                        jint mode, jlong lastModified,
                                        jboolean usemmap)
{
    const char *path = JNU_GetStringPlatformChars(env, name, 0);
    char *msg = 0;
    jlong result = 0;
    int flag = 0;
    jzfile *zip = 0;
    if (mode & OPEN_READ) flag |= O_RDONLY;
    if (path != 0) {
        //---> 注释1 
        zip = ZIP_Get_From_Cache(path, &msg, lastModified);
        if (zip == 0 && msg == 0) {
            ZFILE zfd = 0;
                        //---> 注释2
            zfd = JVM_Open(path, flag, 0);
                ......          
            //--->注释3
            zip = ZIP_Put_In_Cache0(env, thiz, path, zfd, &msg, lastModified, usemmap);
        }
        if (zip != 0) {
            //--->注释4
            result = ptr_to_jlong(zip);
        } 
    }
    return result;
}
  1. 注释1,尝试从缓存中读取文件
  2. 注释2,从磁盘打开文件并返回文件描述符。
  3. 注释3,粗略解析ZIP文件信息并将ZIP文件放入缓存。
  4. 注释4,将zip对应的指针转换成long型的数据并返回。

Zip_util.c#ZIP_Put_In_Cache0

ZIP_Put_In_Cache0(JNIEnv *env, jobject thiz, const char *name, ZFILE zfd, char **pmsg, jlong lastModified,
                 jboolean usemmap)
{
    char errbuf[256];
    jlong len;
    jzfile *zip;
    if ((zip = allocZip(name)) == NULL) {
        return NULL;
    }

    zip->refs = 1;
    zip->lastModified = lastModified;
        .......
    len = zip->len = IO_Lseek(zfd, 0, SEEK_END);
    ......
    zip->zfd = zfd;
    //--->注释1
    if (readCEN(env, thiz, zip, -1) < 0) {
            ......
    }
    
        //--->注释2
    zip->next = zfiles;
    zfiles = zip;
    MUNLOCK(zfiles_lock);
  
    //--->注释3
    return zip;
}
  1. 注释1,读取ZIP文件中央目录区数据,并对中央目录区数据做粗略的解析。
  2. 注释2,将zip结构体对象加入到链表中,相当于是缓存ZIP文件的对象。
  3. 注释3,返回指向ZIP文件的指针。
readCEN(JNIEnv *env, jobject thiz, jzfile *zip, jint knownTotal)
{
    /* Following are unsigned 32-bit */
    jlong endpos, end64pos, cenpos, cenlen, cenoff;
    /* Following are unsigned 16-bit */
    jint total, tablelen, i, j;
    unsigned char *cenbuf = NULL;
    unsigned char *cenend;
    unsigned char *cp;
    unsigned char endbuf[ENDHDR];
    jint endhdrlen = ENDHDR;
    jzcell *entries;
    jint *table;
    /* Clear previous zip error */
    zip->msg = NULL;
    
    //--->注释1
    if ((endpos = findEND(zip, endbuf)) == -1)
        return -1; /* no END header or system error */
    ......
    //--->注释2  
    cenlen = ENDSIZ(endbuf); /* 中央目录区大小 */
    cenoff = ENDOFF(endbuf); /* 中央目录区偏移 */
    total  = ENDTOT(endbuf); /* 中央目录区总的file header数量 */
        ......
    cenpos = endpos - cenlen;/* 中央目录区起始位置*/
    zip->locpos = cenpos - cenoff; /*本地文件头起始位置*/
        ......
    {
        if ((cenbuf = malloc((size_t) cenlen)) == NULL ||
            //--->注释3 
            (readFullyAt(zip->zfd, cenbuf, cenlen, cenpos) == -1))
        goto Catch;
    }
    cenend = cenbuf + cenlen; /*中央目录区结束位置的指针*/
    
    total = (knownTotal != -1) ? knownTotal : total;
    entries  = zip->entries  = calloc(total, sizeof(entries[0]));
    tablelen = zip->tablelen = ((total/2) | 1); // Odd -> fewer collisions
    table    = zip->table    = malloc(tablelen * sizeof(table[0]));
    if ((entries == NULL && total != 0) || table == NULL) goto Catch;
    for (j = 0; j < tablelen; j++)
        table[j] = ZIP_ENDCHAIN;
        ......
    
    //--->注释4
    for (i = 0, cp = cenbuf; cp <= cenend - CENHDR; i++, cp += CENSIZE(cp)) {
        jint method, nlen, flag;
        unsigned int hsh;
                ......
        method = CENHOW(cp);
        nlen   = CENNAM(cp);
                ......
        const char* entryName = (const char *)cp + CENHDR;
            ......
        
        entries[i].cenpos = cenpos + (cp - cenbuf);
        entries[i].hash = hashN(entryName, nlen);
        entries[i].next = ZIP_ENDCHAIN;
        /* Add the entry to the hash table */
        hsh = entries[i].hash % tablelen;
        /* First check that there are no other entries that have the same name. */
        int chain = table[hsh];
        while (chain != ZIP_ENDCHAIN) {
            const jzcell* cell = &entries[chain];
            if (cell->hash == entries[i].hash) {
                const char* cenStart = (const char *) cenbuf + cell->cenpos - cenpos;
                if (CENNAM(cenStart) == nlen) {
                    const char* chainName = cenStart + CENHDR;
                    if (strncmp(entryName, chainName, nlen) == 0) {
                        ZIP_FORMAT_ERROR("invalid CEN header (duplicate entry)");
                    }
                }
            }
            chain = cell->next;
        }
        entries[i].next = table[hsh];
        table[hsh] = i;
    }
        
    //---> 注释5
    zip->total = i;
        ......
    //---> 注释6  
    return cenpos;
}
  1. 注释1,读取中央目录结束标识
  2. 注释2,从中央目录结束标识中读取中央目录长度,中央目录偏移,中央目录数量
  3. 注释3,从ZIP文件中读取中央目录区数据。
  4. 注释4,遍历中央目录区下面的所有file header,获取初步信息,并存储在对应的entries[i]中。
  5. 注释5,记录中央目录区所有的file header数目。
  6. 注释6,返回中央目录区的起始位置,在open方法中。

通过open方法,可以获取到一个代表压缩文件的ZIP对象,也就是我们Java对象ZipFile的jzfile成员变量,同时在open方法中对ZIP文件的中央目录区做了粗略解析,并没有对中央目录区做完全解析,这点应该是出于性能考虑,因为这里只是open方法,不能耗时太多,也没有必要对文件做完全解析。

通过上面分析可以看到,ZipEntryIterator中的getNextEntry方法,其实是一个native方法,该方法实现位于ZipFile.c中,通过JNI中java方法和Natvie方法的对应关系,可以看到native层对应的方法是ZipFile_getNextEntry。

ZipFile.c#ZipFile_getNextEntry

ZipFile_getNextEntry(JNIEnv *env, jclass cls, jlong zfile, jint n)
{
    //--->注释1
    jzentry *ze = ZIP_GetNextEntry(jlong_to_ptr(zfile), n);
    return ptr_to_jlong(ze);
}

JNIEXPORT jzentry * ZIP_GetNextEntry(jzfile *zip, jint n)
{
    jzentry *result;
        ......
    //--->注释2  
    result = newEntry(zip, &zip->entries[n], ACCESS_SEQUENTIAL);
    ......
    return result;
}
  1. 注释1,调用了ZIP_GetNextEntry方法来获取一个entry对象。
  2. 注释2,调用newEntry方法来得到一个全解析的entry对象。

这里的entry是通过解析中央目录区得到的,对应的其实就是中央目录区的每一个file header。

zip_util.c#newEntry

static jzentry * newEntry(jzfile *zip, jzcell *zc, AccessHint accessHint)
{
    jlong locoff;
    jint nlen, elen, clen;
    jzentry *ze;
    char *cen;
    if ((ze = (jzentry *) malloc(sizeof(jzentry))) == NULL) return NULL;
    ze->name    = NULL;
    ze->extra   = NULL;
    ze->comment = NULL;
    {
        if (accessHint == ACCESS_RANDOM)
            cen = readCENHeader(zip, zc->cenpos, AMPLE_CEN_HEADER_SIZE);
        else
            cen = sequentialAccessReadCENHeader(zip, zc->cenpos);
        if (cen == NULL) goto Catch;
    }
    nlen      = CENNAM(cen);
    elen      = CENEXT(cen);
    clen      = CENCOM(cen);
    ze->time  = CENTIM(cen);
    ze->size  = CENLEN(cen);
    ze->csize = (CENHOW(cen) == STORED) ? 0 : CENSIZ(cen);
    ze->crc   = CENCRC(cen);
    locoff    = CENOFF(cen);
    ze->pos   = -(zip->locpos + locoff);
    ze->flag  = CENFLG(cen);
    if ((ze->name = malloc(nlen + 1)) == NULL) goto Catch;
    memcpy(ze->name, cen + CENHDR, nlen);
    ze->name[nlen] = '\0';
    ze->nlen = nlen;
    if (elen > 0) {
        char *extra = cen + CENHDR + nlen;
        /* This entry has "extra" data */
        if ((ze->extra = malloc(elen + 2)) == NULL) goto Catch;
        ze->extra[0] = (unsigned char) elen;
        ze->extra[1] = (unsigned char) (elen >> 8);
        memcpy(ze->extra+2, extra, elen);
        ......
    }
    if (clen > 0) {
        /* This entry has a comment */
        if ((ze->comment = malloc(clen + 1)) == NULL) goto Catch;
        memcpy(ze->comment, cen + CENHDR + nlen + elen, clen);
        ze->comment[clen] = '\0';
    }
        ......
    return ze;
}

这个方法是解析中央目录区的所有的file header,并构建该file header对应的entry对象。所以,这里才是详细解析中央目录区file header的过程,而open只是粗略解析了中央目录区的信息。

这个方法最终返回了ze,其实就是代表file header的对象。并最终将这个对象的地址通过ZipFile_getNextEntry方法(对应的Java方法是getNextEntry)返回给了Java调用层,即ZipEntryIterator#next方法中,调用getNextEntry返回的jzentry局部变量

然后,调用ZipFile#getZipEntry方法,并传入jzentry,getZipEntry方法中调用native方法来解析中央目录区file header的字段,并构建一个java对象。底层其实就是native层去解析jzentry代表的file header对象。

ZipFile_getEntryCSize(JNIEnv *env, jclass cls, jlong zentry)
{
    jzentry *ze = jlong_to_ptr(zentry);
    return ze->csize != 0 ? ze->csize : ze->size;
}

JNIEXPORT jlong JNICALL ZipFile_getEntrySize(JNIEnv *env, jclass cls, jlong zentry)
{
    jzentry *ze = jlong_to_ptr(zentry);
    return ze->size;
}

方法中ze是一个指针,其实是通过long型的参数jzentry转变而来,因为在native层newEntry方法中已经做了完全解析,所以,这里的get方法直接返回ze对象中对应的字段即可。

到这里,终于把ZipFile解析中央目录区每个file header的过程分析完了,到这里,通过读中央目录区解析出了所有压缩文件在中央目录区对应的file header,后面就是根据这些信息找到对应file data进行解压缩的过程了。

ZipEntry.hasMoreElements
public boolean hasMoreElements() {
    return hasNext();
}

public boolean hasNext() {
    synchronized (ZipFile.this) {
        ensureOpen();
        return i < total;
    }
}

这个方法比较简单,就是判断是否还有待解压的文件,total是通过native方法获取到的,total是在open方法中得到的,就是中央目录区后中所有file header的数目。

ZipFile_getTotal(JNIEnv *env, jclass cls, jlong zfile)
{
    jzfile *zip = jlong_to_ptr(zfile);
    return zip->total;
}
ZipFile#getInputStream
public InputStream getInputStream(ZipEntry entry) throws IOException {
    if (entry == null) {
        throw new NullPointerException("entry");
    }
    long jzentry = 0;
    ZipFileInputStream in = null;
    synchronized (this) {
            
        jzentry = getEntry(jzfile, zc.getBytes(entry.name), true);
        ......  
        //--->注释1  
        in = new ZipFileInputStream(jzentry);

        switch (getEntryMethod(jzentry)) {
        case STORED:
            synchronized (streams) {
                streams.put(in, null);
            }
            return in;
        case DEFLATED:
            //--->注释2
            long size = getEntrySize(jzentry) + 2; // Inflater likes a bit of slack
            if (size > 65536) size = 65536;
            if (size <= 0) size = 4096;
            Inflater inf = getInflater();
            //--->注释3
            InputStream is = new ZipFileInflaterInputStream(in, inf, (int)size);
            synchronized (streams) {
                streams.put(is, inf);
            }
            return is;
    }
}
  1. 注释1,使用ZipFile的内部类ZipFileInputStream构建了输入流,下面重点看下这个输入流的read方法。

    public int read(byte b[], int off, int len) throws IOException {
            ......
        synchronized (ZipFile.this) {
            long rem = this.rem;
            long pos = this.pos;
                ......
            len = ZipFile.read(ZipFile.this.jzfile, jzentry, pos, b, off, len);
            if (len > 0) {
                this.pos = (pos + len);
                this.rem = (rem - len);
            }
        }
            ......
        return len;
    }
    

    这里的ZipFile#read方法是native层方法,所以ZipFileInputStream时通过natvie方法来读流的。

  2. 注释2,这里getEntrySize获取了整个待解压文件的大小。然后很关键的一点,这里对size做了一个判断,如果size>65535(即64k),那么就取65535,如果小于0,就取4096,即4k。然后把这个size传入了ZipFileInflaterInputStream的构造方法,作为ZipFileInflaterInputStream流缓冲区大小。也就是说,在ZipFile向内存中读取压缩数据流的时候,如果实际压缩文件小于64k,那么缓冲区大小就是实际文件大小,如果大于64k,那么缓冲区大小就是64k。而ZipInputStream读取压缩数据流的时候,缓冲区大小是512字节。所以,如果对磁盘上的文件解压,ZipFile的读数据流的速度会比ZipInputStream快很多,操作IO的次数也会减少,效率会提升很多

  3. 注释3,ZipFileInflaterInputStream是继承于InflaterInputStream,所以它解压文件的步骤也是和ZipInputStream的解压流程类似。但是ZipFile最底层的流是ZipFileInputStream,而ZipIputStream底层的流是FileInputStream,这点也是不同的。

ZipFile#getEntry
public ZipEntry getEntry(String name) {
        ......
    long jzentry = 0;
    synchronized (this) {
        ensureOpen();
        jzentry = getEntry(jzfile, zc.getBytes(name), true);
        if (jzentry != 0) {
            ZipEntry ze = getZipEntry(name, jzentry);
            freeEntry(jzfile, jzentry);
            return ze;
        }
    }
    return null;
}

ZipFile还有一个getEntry(String name)方法,这个方法可以只解压ZIP文件中的某个单独的文件,而不解压其他文件,这个ZipInputStream是没有的。其实从ZipFile和ZipInputStream两种解压方式的底层原理也可以明白两者的区别。因为ZipFile是通过读中央目录区的file header读取了所有文件的元数据,而ZipInputStream是从local file header开始顺序读流的,只能按照顺序一个个访问文件。所以,ZipFile可以解压任意的某个文件,而ZipInputStream不具备这个能力。

ZipFile和ZipInputStream对比

ZipFile和ZipInputStream正好对应了前面讨论的两种解压的方式,即通过读取中央目录区解压和通过读取本地文件头解压。因为解压的原理不同,所以适用的场景也有所不同。下面先来看两种解压方式的特点及适用场景。

ZipFile特点及使用场景

ZipFile类通过读中央目录区来获取所有压缩文件的元数据信息。然后通过这些信息来解压所有的文件。

ZipFile特点

  • ZipFile需要通过中央目录区结束标识找到中央目录区数据并解析,因此压缩文件必须是已经存在于磁盘上。
  • ZipFile提供了随机访问ZIP文件内容的能力。这意味着可以直接获取ZIP文件中的任何条目,而不需要按照它们在ZIP文件中的顺序来读取。因此,可以通过名称快速查找ZIP文件中的条目。
  • ZipFile对象被创建后,ZIP文件的信息会被映射到内存中,所以随机访问会非常快。但同时也会占用一些内存。
  • ZipFile在读压缩流数据时,读流缓冲区更大,上限是64K,读取效率更高。

ZipFile适用场景:

  • 文件已经保存在磁盘,并且需要解压全部文件,使用ZipFile效率更高。
  • 文件已经保存在磁盘,只需要解压ZIP文件中的某个文件或者部分文件,使用ZipFile效率更高。
ZipInputStream特点及使用场景

ZipInputStream类是一个输入流,它允许你从ZIP文件中顺序地读取压缩的条目。

ZipInputStream特点

  • 提供了顺序访问ZIP文件内容的能力。它只能按照它们在ZIP文件中的顺序来读取条目。
  • ZipInputStream在处理ZIP文件时不需要将ZIP文件信息映射到内存中,更节省内存。
  • ZipInputStream在读压缩流数据时,读流缓冲区大小是512字节,读取效率相比于ZipFile会低一点。

ZipInputStream适用场景

  • 当文件不在磁盘上,比如从网络接收的数据,想边接收边解压,因ZipInputStream是顺序按流的方式读取文件,使用ZipInputStream更合适。
  • 如果顺序解压ZIP文件的前面的一小部分文件, ZipFile也不是最佳选择,因为ZipFile读中央目录区会带来额外的耗时。
  • 如果ZIP文件中的中央目录区遭到损坏,只能通过ZipInputStream来按顺序解压。

参考

ZIP官方文档

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

推荐阅读更多精彩内容