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官方文檔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容