對于Android 常用的壓縮格式ZIP ,你了解多少?
Android 的有兩種解壓ZIP 的方法,你知道嗎?
ZipFile 和ZipInputStream 的解壓效率,你對比過嗎?
帶著以上問題,現在就開始ZIP的解壓之旅。
1. Zip文件結構
ZIP文件結構如下圖所示, File Entry表示一個文件實體,一個壓縮文件中有多個文件實體。
文件實體由一個頭部和文件數據組,Central Directory由多個File header組成,每個File header都保存一個文件實體的偏移,文件最后由End of central directory結束。
image
1.1 Local File Header
1.2. Data descriptor
當頭部標志第3位(掩碼0×08)置位時,表示CRC-32校驗位和壓縮后大小在File Entry結構的尾部增加一個Data descriptor來記錄。
1.3. Central Directory
Central Directory File Header
End of Central Directory record
所有的File Header結束后是該數據結構
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文件,只想更新幾個小文件,如果采用這種方式效率非常低。
2,ZIP文件解壓
Android提供兩種解壓ZIP文件的方法:ZipFile和ZipInputStream
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中定義了兩個類,分別是RAFStream和ZipInflaterInputStream,這兩個類分別繼承自RandomAccessFile和InflateInputStream,通過getInputStream()返回,ZipFile的解壓流程和ZipInputStream類似。
ZipFile和ZipInputStream真正不同的地方在InflaterInputStream.fill(),fill源碼如下:
protected void fill() throws IOException {
checkClosed();
if (nativeEndBufSize > 0) {
ZipFile.RAFStreamis = (ZipFile.RAFStream) in;
len = is.fill(inf, nativeEndBufSize);
} else {
if ((len = in.read(buf)) > 0) {
inf.setInput(buf, 0, len);
}
}
}
下面同樣給出InflaterInputStream.read()的流程圖,大家就能明白二者的區別之處。
從流程圖可以看出,ZipFile的讀文件是在native層進行的,每次讀文件的大小是由java層傳入的,定義如下:
Math.max(1024, (**int**) Math.min(entry.getSize(), 65535L));
即ZipFile每次處理的數據大小在1KB和64KB之間,如果文件大小介于二者之間,則可以一次將文件處理完。而對于ZipInputStream來說,每次能處理的數據只能是512個字節,所以ZipFile的解壓效率更高。
3,ZipFile vs ZipInputStream效率對比
解壓文件可以分三步:
1,從磁盤讀出zip文件
2,調用inflate解壓出數據
3,存儲解壓后的數據
因此兩者的效率對比可以細化到這三個步驟來對比。
3.1 讀磁盤
ZipFile在native層讀文件,并且每次讀的數據在1KB~64KB之間,ZipInputStream只有采用更大的Buffer才可能達到ZipFile的性能。
3.2 infalte解壓效率
從上文可知,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進行測試,文件說明如下。
測試數據:
結論:1,ZipFile的read調用的次數減少39%~93%,可以看出ZipFile的解壓效率更高
2,ZipFile解壓文件耗時,相比ZipInputStream有22%到73%的減少
3.3 存儲解壓后的數據
從上文可以知道,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);
}
}
同樣對上面的兩個壓縮文件進行解壓,測試數據如下:
結論: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來按順序解壓。
4,結論
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);
}
}