對于Android 常用的壓縮格式ZIP 笋庄,你了解多少物延?
Android 的有兩種解壓ZIP 的方法学搜,你知道嗎垫桂?
ZipFile 和ZipInputStream 的解壓效率蜡励,你對比過嗎隆圆?
帶著以上問題,現(xiàn)在就開始ZIP的解壓之旅镀岛。
1. Zip文件結構
ZIP文件結構如下圖所示弦牡, File Entry表示一個文件實體,一個壓縮文件中有多個文件實體。
文件實體由一個頭部和文件數(shù)據(jù)組漂羊,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結束后是該數(shù)據(jù)結構
Q1:Central Directory的作用
通過Central Directory可以快速獲取ZIP包含的文件列表旨指,而不用逐個掃描文件赏酥,雖然Central Directory的內容和文件原來的頭文件有冗余,但是當zip文件被追加到其他文件時谆构,就只能通過Central Directory獲取ZIP信息裸扶,而不能通過掃描文件的方式,因為central directory可能聲明一些文件被刪除或者已經更新搬素。Central Directory中Entry的順序可以和文件的實際順序不一樣呵晨。
Q2:ZIP如何更新文件
舉例說明:一個ZIP包含A、B和C三個文件熬尺,現(xiàn)在準備刪除文件B摸屠,并且對C進行了更新,可以將新的文件C 添加到原來ZIP的后面粱哼,同時添加一個新的Central Directory季二,僅僅包含文件A和新文件C,這樣就實現(xiàn)了刪除文件B和更新文件C皂吮。
在ZIP設計之初戒傻,通過軟盤來移動文件很常見税手,但是讀寫磁盤是很消耗性能的蜂筹,對于一個很大的ZIP文件需纳,只想更新幾個小文件,如果采用這種方式效率非常低艺挪。
2不翩,ZIP文件解壓
Android提供兩種解壓ZIP文件的方法:ZipFile和ZipInputStream
2.1 ZipInputStream
ZipInputStream通過流式來順序訪問ZIP,當讀到某個文件結尾時(Entry)返回-1麻裳,通過getNextEntry來判斷是否要繼續(xù)向下讀口蝠,ZipInputStream 的read方法的流程圖如下。
Q3****:為什么要判斷是否是壓縮文件津坑?
因為文件在添加到ZIP時妙蔗,可以通過設置Entry.setMethod(ZipEntry.STORED)以非壓縮的形式添加到文件,所以在解壓時疆瑰,對于這種情況眉反,可以直接讀文件返回,不需要要解壓穆役。
這里要重點介紹一下InflaterInputStream.read()方法寸五,其流程圖如下。
從流程圖可以看出耿币,java層將待解壓的數(shù)據(jù)通過我們定義的Buffer傳入native層梳杏。每次傳入的數(shù)據(jù)大小是固定值為512字節(jié),在InflaterInputStream.java中定義如下:
static** **final** **int** **BUF_SIZE** = 512;
對于壓縮文件來說淹接,最終會調用zlib中的inflate.c來解壓文件十性,inflate.c通過狀態(tài)機來對文件進行解壓,將解壓后的數(shù)據(jù)再通過Buffer返回塑悼。對inflate解壓算法感興趣的同學可以看源碼劲适,
傳送門:http://androidxref.com/4.4.4_r1/xref/external/zlib/src/inflate.c
返回count字節(jié)并不等于buffer的大小,取決于inflate解壓返回的數(shù)據(jù)拢肆。
2.2 ZipFile
ZipFile通過RandomAccessFile隨機訪問zip文件减响,通過Central Directory得到zip中所有的Entry, Entry中包含文件的開始位置和size郭怪,前期讀Central Directory可能會耗費一些時間支示,但是后面就可以利用RandomAccessFile的特性,每次讀入更多的數(shù)據(jù)來提高解壓效率鄙才。
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()的流程圖栽渴,大家就能明白二者的區(qū)別之處尖坤。
從流程圖可以看出,ZipFile的讀文件是在native層進行的闲擦,每次讀文件的大小是由java層傳入的慢味,定義如下:
Math.max(1024, (**int**) Math.min(entry.getSize(), 65535L));
即ZipFile每次處理的數(shù)據(jù)大小在1KB和64KB之間,如果文件大小介于二者之間墅冷,則可以一次將文件處理完纯路。而對于ZipInputStream來說,每次能處理的數(shù)據(jù)只能是512個字節(jié)寞忿,所以ZipFile的解壓效率更高驰唬。
3,ZipFile vs ZipInputStream效率對比
解壓文件可以分三步:
1腔彰,從磁盤讀出zip文件
2叫编,調用inflate解壓出數(shù)據(jù)
3,存儲解壓后的數(shù)據(jù)
因此兩者的效率對比可以細化到這三個步驟來對比萍桌。
3.1 讀磁盤
ZipFile在native層讀文件宵溅,并且每次讀的數(shù)據(jù)在1KB~64KB之間,ZipInputStream只有采用更大的Buffer才可能達到ZipFile的性能上炎。
3.2 infalte解壓效率
從上文可知恃逻,inflate每次解壓的數(shù)據(jù)是不定的,一方面和inflate的解壓算法有關藕施,另一方面取決native層infalte.c每次處理的數(shù)據(jù)寇损,以上分析可以,ZipInputStream每次只傳遞512字節(jié)數(shù)據(jù)到native層裳食,而ZipFile每次傳遞的數(shù)據(jù)可以在1KB~64KB矛市,所以ZipFile的解壓效率更高。從java_util_zip_Inflater.cpp源碼看诲祸,這是Android做的特別優(yōu)化浊吏。
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進行測試,文件說明如下救氯。
測試數(shù)據(jù):
結論:1找田,ZipFile的read調用的次數(shù)減少39%~93%,可以看出ZipFile的解壓效率更高
2着憨,ZipFile解壓文件耗時墩衙,相比ZipInputStream有22%到73%的減少
3.3 存儲解壓后的數(shù)據(jù)
從上文可以知道,inflate解壓后返回的數(shù)據(jù)可能會小于buffer的長度,如果每次在read返回后就直接寫文件漆改,此時buffer可能并沒有充滿心铃,造成buffer的利用效率不高,此處可以考慮將解壓出的數(shù)據(jù)輸出到BufferedOutputStream挫剑,等buffer滿后再寫入文件去扣,這樣做的弊端是,因為要湊滿buffer暮顺,會導致read的調用次數(shù)增加厅篓,下面就對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);
}
}
同樣對上面的兩個壓縮文件進行解壓捶码,測試數(shù)據(jù)如下:
結論:1,ZipFile較ZipInputStream相比或链,耗時仍有15%-22%的減少
2惫恼,與不使用Buffer相比,ZipInputStream的耗時減少14%-62%澳盐,ZipFile解壓低壓縮率文件耗時有6%的減少祈纯,但是對于高壓縮率,耗時將有9%的增加(雖然減少了寫磁盤的次數(shù)叼耙,但是為了湊足buffer腕窥,增加了read的調用次數(shù),導致整體耗時增加)
Q4:那么問題來了筛婉,既然ZipFile效率這么好簇爆,那ZipInputStream還有存在的價值嗎?
千萬別被數(shù)據(jù)迷惑了雙眼爽撒,上面的測試僅僅是覆蓋了一種場景入蛆,即:文件已經在磁盤中存在,且需全部解壓出ZIP中的文件硕勿,如果你的場景符合以上兩點哨毁,使用ZipFile無疑是正確無比。同時源武,也可以利用ZipFile的隨機訪問能力扼褪,實現(xiàn)解壓ZIP中間的某幾個文件。
但是在以下場景粱栖,ZipFile則會略顯無力话浇,這是ZipInputStream價值就體現(xiàn)出來了:
1,當文件不在磁盤上查排,比如從網絡接收的數(shù)據(jù)凳枝,想邊接收邊解壓,因ZipInputStream是順序按流的方式讀取文件,這種場景實現(xiàn)起來毫無壓力岖瑰。
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);
}
}