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







免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM