<技術[轉]>Android 解壓zip文件

轉自:Android 解壓zip文件你知道多少募谎?

  • 對于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


image.png

1.2. Data descriptor
當頭部標志第3位(掩碼0×08)置位時椭豫,表示CRC-32校驗位和壓縮后大小在File Entry結構的尾部增加一個Data descriptor來記錄。


image.png

1.3. Central Directory
Central Directory File Header

image.png

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


image.png

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方法的流程圖如下。

image.png

Q3****:為什么要判斷是否是壓縮文件津坑?

因為文件在添加到ZIP時妙蔗,可以通過設置Entry.setMethod(ZipEntry.STORED)以非壓縮的形式添加到文件,所以在解壓時疆瑰,對于這種情況眉反,可以直接讀文件返回,不需要要解壓穆役。

這里要重點介紹一下InflaterInputStream.read()方法寸五,其流程圖如下。

image

從流程圖可以看出耿币,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ū)別之處尖坤。


image

從流程圖可以看出,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);
    }
 }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末疾党,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惨奕,更是在濱河造成了極大的恐慌雪位,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梨撞,死亡現(xiàn)場離奇詭異雹洗,居然都是意外死亡,警方通過查閱死者的電腦和手機卧波,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門时肿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人港粱,你說我怎么就攤上這事螃成〉┣” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵寸宏,是天一觀的道長宁炫。 經常有香客問我,道長氮凝,這世上最難降的妖魔是什么羔巢? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮罩阵,結果婚禮上竿秆,老公的妹妹穿的比我還像新娘。我一直安慰自己稿壁,他們只是感情好幽钢,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著常摧,像睡著了一般搅吁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音姨伤,去河邊找鬼谷异。 笑死,一個胖子當著我的面吹牛吸申,可吹牛的內容都是我干的梗劫。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼截碴,長吁一口氣:“原來是場噩夢啊……” “哼梳侨!你這毒婦竟也來了?” 一聲冷哼從身側響起日丹,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤走哺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后哲虾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丙躏,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年束凑,在試婚紗的時候發(fā)現(xiàn)自己被綠了晒旅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡汪诉,死狀恐怖废恋,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤鱼鼓,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布孝常,位于F島的核電站,受9級特大地震影響蚓哩,放射性物質發(fā)生泄漏构灸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一岸梨、第九天 我趴在偏房一處隱蔽的房頂上張望喜颁。 院中可真熱鬧,春花似錦曹阔、人聲如沸半开。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寂拆。三九已至,卻和暖如春抓韩,著一層夾襖步出監(jiān)牢的瞬間纠永,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工谒拴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尝江,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓英上,卻偏偏與公主長得像炭序,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子苍日,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容