轉(zhuǎn)自:Android 解壓zip文件你知道多少?
對(duì)于Android 常用的壓縮格式ZIP ,你了解多少?
Android 的有兩種解壓ZIP 的方法,你知道嗎?
ZipFile 和ZipInputStream 的解壓效率,你對(duì)比過嗎?
帶著以上問題,現(xiàn)在就開始ZIP的解壓之旅。
1. Zip文件結(jié)構(gòu)
ZIP文件結(jié)構(gòu)如下圖所示, File Entry表示一個(gè)文件實(shí)體,一個(gè)壓縮文件中有多個(gè)文件實(shí)體。
文件實(shí)體由一個(gè)頭部和文件數(shù)據(jù)組,Central Directory由多個(gè)File header組成,每個(gè)File header都保存一個(gè)文件實(shí)體的偏移,文件最后由End of central directory結(jié)束。
image

1.1 Local File Header

1.2. Data descriptor
當(dāng)頭部標(biāo)志第3位(掩碼0×08)置位時(shí),表示CRC-32校驗(yàn)位和壓縮后大小在File Entry結(jié)構(gòu)的尾部增加一個(gè)Data descriptor來記錄。

1.3. Central Directory
Central Directory File Header

End of Central Directory record
所有的File Header結(jié)束后是該數(shù)據(jù)結(jié)構(gòu)

Q1:Central Directory的作用
通過Central Directory可以快速獲取ZIP包含的文件列表,而不用逐個(gè)掃描文件,雖然Central Directory的內(nèi)容和文件原來的頭文件有冗余,但是當(dāng)zip文件被追加到其他文件時(shí),就只能通過Central Directory獲取ZIP信息,而不能通過掃描文件的方式,因?yàn)閏entral directory可能聲明一些文件被刪除或者已經(jīng)更新。Central Directory中Entry的順序可以和文件的實(shí)際順序不一樣。
Q2:ZIP如何更新文件
舉例說明:一個(gè)ZIP包含A、B和C三個(gè)文件,現(xiàn)在準(zhǔn)備刪除文件B,并且對(duì)C進(jìn)行了更新,可以將新的文件C 添加到原來ZIP的后面,同時(shí)添加一個(gè)新的Central Directory,僅僅包含文件A和新文件C,這樣就實(shí)現(xiàn)了刪除文件B和更新文件C。
在ZIP設(shè)計(jì)之初,通過軟盤來移動(dòng)文件很常見,但是讀寫磁盤是很消耗性能的,對(duì)于一個(gè)很大的ZIP文件,只想更新幾個(gè)小文件,如果采用這種方式效率非常低。
2,ZIP文件解壓
Android提供兩種解壓ZIP文件的方法:ZipFile和ZipInputStream
2.1 ZipInputStream
ZipInputStream通過流式來順序訪問ZIP,當(dāng)讀到某個(gè)文件結(jié)尾時(shí)(Entry)返回-1,通過getNextEntry來判斷是否要繼續(xù)向下讀,ZipInputStream 的read方法的流程圖如下。

Q3****:為什么要判斷是否是壓縮文件?
因?yàn)槲募谔砑拥絑IP時(shí),可以通過設(shè)置Entry.setMethod(ZipEntry.STORED)以非壓縮的形式添加到文件,所以在解壓時(shí),對(duì)于這種情況,可以直接讀文件返回,不需要要解壓。
這里要重點(diǎn)介紹一下InflaterInputStream.read()方法,其流程圖如下。

從流程圖可以看出,java層將待解壓的數(shù)據(jù)通過我們定義的Buffer傳入native層。每次傳入的數(shù)據(jù)大小是固定值為512字節(jié),在InflaterInputStream.java中定義如下:
static** **final** **int** **BUF_SIZE** = 512;
對(duì)于壓縮文件來說,最終會(huì)調(diào)用zlib中的inflate.c來解壓文件,inflate.c通過狀態(tài)機(jī)來對(duì)文件進(jìn)行解壓,將解壓后的數(shù)據(jù)再通過Buffer返回。對(duì)inflate解壓算法感興趣的同學(xué)可以看源碼,
傳送門:http://androidxref.com/4.4.4_r1/xref/external/zlib/src/inflate.c
返回count字節(jié)并不等于buffer的大小,取決于inflate解壓返回的數(shù)據(jù)。
2.2 ZipFile
ZipFile通過RandomAccessFile隨機(jī)訪問zip文件,通過Central Directory得到zip中所有的Entry, Entry中包含文件的開始位置和size,前期讀Central Directory可能會(huì)耗費(fèi)一些時(shí)間,但是后面就可以利用RandomAccessFile的特性,每次讀入更多的數(shù)據(jù)來提高解壓效率。
ZipFile中定義了兩個(gè)類,分別是RAFStream和ZipInflaterInputStream,這兩個(gè)類分別繼承自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層進(jìn)行的,每次讀文件的大小是由java層傳入的,定義如下:
Math.max(1024, (**int**) Math.min(entry.getSize(), 65535L));
即ZipFile每次處理的數(shù)據(jù)大小在1KB和64KB之間,如果文件大小介于二者之間,則可以一次將文件處理完。而對(duì)于ZipInputStream來說,每次能處理的數(shù)據(jù)只能是512個(gè)字節(jié),所以ZipFile的解壓效率更高。
3,ZipFile vs ZipInputStream效率對(duì)比
解壓文件可以分三步:
1,從磁盤讀出zip文件
2,調(diào)用inflate解壓出數(shù)據(jù)
3,存儲(chǔ)解壓后的數(shù)據(jù)
因此兩者的效率對(duì)比可以細(xì)化到這三個(gè)步驟來對(duì)比。
3.1 讀磁盤
ZipFile在native層讀文件,并且每次讀的數(shù)據(jù)在1KB~64KB之間,ZipInputStream只有采用更大的Buffer才可能達(dá)到ZipFile的性能。
3.2 infalte解壓效率
從上文可知,inflate每次解壓的數(shù)據(jù)是不定的,一方面和inflate的解壓算法有關(guān),另一方面取決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****驗(yàn)證(關(guān)鍵代碼):
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****關(guān)鍵代碼:
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);
}
}
我們用兩個(gè)不同壓縮率的文件對(duì)demo進(jìn)行測(cè)試,文件說明如下。

測(cè)試數(shù)據(jù):

結(jié)論:1,ZipFile的read調(diào)用的次數(shù)減少39%~93%,可以看出ZipFile的解壓效率更高
2,ZipFile解壓文件耗時(shí),相比ZipInputStream有22%到73%的減少
3.3 存儲(chǔ)解壓后的數(shù)據(jù)
從上文可以知道,inflate解壓后返回的數(shù)據(jù)可能會(huì)小于buffer的長(zhǎng)度,如果每次在read返回后就直接寫文件,此時(shí)buffer可能并沒有充滿,造成buffer的利用效率不高,此處可以考慮將解壓出的數(shù)據(jù)輸出到BufferedOutputStream,等buffer滿后再寫入文件,這樣做的弊端是,因?yàn)橐獪悵Mbuffer,會(huì)導(dǎo)致read的調(diào)用次數(shù)增加,下面就對(duì)ZipFile和Zipinputstream做一個(gè)對(duì)比。
demo(關(guān)鍵代碼):
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);
}
}
同樣對(duì)上面的兩個(gè)壓縮文件進(jìn)行解壓,測(cè)試數(shù)據(jù)如下:

結(jié)論:1,ZipFile較ZipInputStream相比,耗時(shí)仍有15%-22%的減少
2,與不使用Buffer相比,ZipInputStream的耗時(shí)減少14%-62%,ZipFile解壓低壓縮率文件耗時(shí)有6%的減少,但是對(duì)于高壓縮率,耗時(shí)將有9%的增加(雖然減少了寫磁盤的次數(shù),但是為了湊足buffer,增加了read的調(diào)用次數(shù),導(dǎo)致整體耗時(shí)增加)
Q4:那么問題來了,既然ZipFile效率這么好,那ZipInputStream還有存在的價(jià)值嗎?
千萬別被數(shù)據(jù)迷惑了雙眼,上面的測(cè)試僅僅是覆蓋了一種場(chǎng)景,即:文件已經(jīng)在磁盤中存在,且需全部解壓出ZIP中的文件,如果你的場(chǎng)景符合以上兩點(diǎn),使用ZipFile無疑是正確無比。同時(shí),也可以利用ZipFile的隨機(jī)訪問能力,實(shí)現(xiàn)解壓ZIP中間的某幾個(gè)文件。
但是在以下場(chǎng)景,ZipFile則會(huì)略顯無力,這是ZipInputStream價(jià)值就體現(xiàn)出來了:
1,當(dāng)文件不在磁盤上,比如從網(wǎng)絡(luò)接收的數(shù)據(jù),想邊接收邊解壓,因ZipInputStream是順序按流的方式讀取文件,這種場(chǎng)景實(shí)現(xiàn)起來毫無壓力。
2,如果順序解壓ZIP前面的一小部分文件, ZipFile也不是最佳選擇,因?yàn)閆ipFile讀CentralDirectory會(huì)帶來額外的耗時(shí)。
3,如果ZIP中CentralDirectory遭到損壞,只能通過ZipInputStream來按順序解壓。
4,結(jié)論
1,如果ZIP文件已保存在磁盤,且解壓ZIP中的所有文件,建議用ZipFile,效率較ZipInputStream有15%~27%的提升。
2,僅解壓ZIP中間的某些文件,建議用ZipFile
3,如果ZIP沒有在磁盤上或者順序解壓一小部分文件,又或ZIP文件目錄遭到損壞,建議用ZipInputStream
從以上分析和驗(yàn)證可以看出,同一種解壓方法使用的方式不同,效率也會(huì)相差甚遠(yuǎn),最后再回顧一下ZipInputStream和ZipFile最高效的用法(紅色為關(guān)鍵部分)。
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);
}
}