對於Android經常使用的壓縮格式ZIP,你瞭解多少?html
Android的有兩種解壓ZIP的方法,你知道嗎?java
ZipFile和ZipInputStream的解壓效率,你對比過嗎?android
帶着以上問題,如今就開始ZIP的解壓之旅。算法
ZIP文件結構以下圖所示, File Entry表示一個文件實體,一個壓縮文件中有多個文件實體。網絡
文件實體由一個頭部和文件數據組,Central Directory由多個File header組成,每一個File header都保存一個文件實體的偏移,文件最後由End of central directory結束。數據結構
偏移dom |
字節數ide |
描述性能 |
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 |
擴展字段 |
當頭部標誌第3位(掩碼0×08)置位時,表示CRC-32校驗位和壓縮後大小在File Entry結構的尾部增長一個Data descriptor來記錄。
偏移 |
字節數 |
描述 |
0 |
0/4 |
固定值0x08074b50 |
0/4 |
4 |
CRC-32校驗 |
4/8 |
4 |
壓縮後大小 |
8/12 |
4 |
壓縮前大小 |
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文件註釋 |
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文件,只想更新幾個小文件,若是採用這種方式效率很是低。
Android提供兩種解壓ZIP文件的方法:ZipFile和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解壓返回的數據。
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源碼以下:
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, (int) Math.min(entry.getSize(), 65535L));
即ZipFile每次處理的數據大小在1KB和64KB之間,若是文件大小介於兩者之間,則能夠一次將文件處理完。而對於ZipInputStream來講,每次能處理的數據只能是512個字節,因此ZipFile的解壓效率更高。
解壓文件能夠分三步:
1,從磁盤讀出zip文件
2,調用inflate解壓出數據
3,存儲解壓後的數據
所以二者的效率對比能夠細化到這三個步驟來對比。
ZipFile在native層讀文件,而且每次讀的數據在1KB~64KB之間,ZipInputStream只有採用更大的Buffer纔可能達到ZipFile的性能。
從上文可知,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進行測試,文件說明以下。
組成 |
壓縮前size(MB) |
壓縮後size(MB) |
壓縮率 |
|
低壓縮率ZIP |
4個文本文件 |
17 |
1.25 |
7% |
高壓縮率ZIP |
100個jpg圖片 |
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% |
結論:1,ZipFile的read調用的次數減小39%~93%,能夠看出ZipFile的解壓效率更高
2,ZipFile解壓文件耗時,相比ZipInputStream有22%到73%的減小
從上文能夠知道,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);
} }
一樣對上面的兩個壓縮文件進行解壓,測試數據以下:
低壓縮率(ms) |
高壓縮率(ms) |
|
ZipInputStream |
930.2 |
1347.2 |
ZipFile |
794.5 |
1056.8 |
ZipFile耗時減小 |
15% |
22% |
結論: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來按順序解壓。
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);} }