本文將介紹一個針對Tar包解析時的優化方案,旨在優化內存、提高效率。
一、首先講一個tar包的文件結構。(懂得可以繞開此段)
tar只是一個歸檔文件,並不進行壓縮。
struct tar_header
{
char name[100];
char mode[8];
char uid[8];
char gid[8];
char size[12];
char mtime[12];
char chksum[8];
char typeflag;
char linkname[100];
char magic[6];
char version[2];
char uname[32];
char gname[32];
char devmajor[8];
char devminor[8];
char prefix[155];
char padding[12];
};
以上是Tar中保存文件信息的數據結構,其後跟着的就是文件的內容。
size爲文件大小的八進制字節表示,例如文件大小爲90個字節,那麼這裏就是八進制的90,即爲132。
其中,文件大小,修改時間,checksum都是存儲的對應的八進制字符串,字符串最後一個字符爲空格字符
checksum的計算方法爲出去checksum字段其他所有的512-8共504個字節的ascii碼相加的值再加上256(checksum當作八個空格,即8*0x20)
文件內容以512字節爲一個block進行分割,最後一個block不足部分以0補齊
兩個文件的tar包首先存放第一個文件的tar頭結構,然後存儲文件內容,接着存儲第二個文件的tar頭結構,然後存儲文件內容
所有文件都存儲完了以後,最後存放一個全零的tar結構
所有的tar文件大小應該都是512的倍數,一個空文件打包後爲512*3字節,包括一個tar結構頭,一個全零的block存儲文件內容,一個全零的tar結構
檢測tar文件格式的方法:
1、檢測magic字段,即在0x101處檢查字符串,是否爲ustar。有時某些壓縮軟件將這個字段設置爲空。如果magic字段爲空,進入第2步。
2、計算校驗和,按照上面的方法計算校驗和,如果校驗和正確的話,那麼這就是一個tar文件。
注意:在windows下面,不支持uid、uname等,有的甚至不支持magic,這樣就比較麻煩了。
二、Java層普遍的“解壓”方式
因爲在jdk中提供了 FilterInputStream,因此我們可以通過繼承該類,並構造一個TarEntry的模板,在子類中按每512個字節,將一個tar流分成包含N個512字節的TarEntry. 這樣我們就可以將一個tar包通過TarInputStream和TarEntry解開到一個map集合中<entryName,data>.
三、內存優化的 “解壓”方式
由於每一個TarEntry都是一個固定大小字節的對象,那麼我們可不可以直接讀取這塊內存,而不是將所有都常駐內存呢?
答案當然是可以的。
爲了內存上的優化和效率上的提升,我們可以直接讀取指定EntryNam的內存塊。
因爲一個tar包基本的組成結構就是 entryName->data。我們可以拿到每一個EntryName和其對應的內存大小、偏移量,在讀取的時候直接在TarInputStream中讀取相應內存塊。
代碼如下:
1、 一個簡單維護TarEntry偏移量和字節大小的類McTarEntry。
public class McTarEntry {
private long offset;
private int size;
private McTarEntry(Builder builder) {
offset = builder.offset;
size = builder.size;
}
public long getOffset() {
return offset;
}
public int getSize() {
return size;
}
public static class Builder {
private long offset = 0;
private int size = 0;
public Builder offset(long offset) {
this.offset = offset;
return this;
}
public Builder size(int size) {
this.size = size;
return this;
}
public H5TarEntry build() {
return new McTarEntry(this);
}
}
}
2、解析Tar包,將每個McTarEntry保存在map 。
FileInputStream fis = new FileInputStream(tarPath);
BufferedInputStream bis = new BufferedInputStream(fis);
TarInputStream tis = new TarInputStream(bis);
TarEntry te = null;
while ((te = tis.getNextEntry()) != null) {
String entryName = te.getName();
if (te.isDirectory() || TextUtils.isEmpty(entryName)) {
continue;
}
McTarEntry mcTarEntry = new McTarEntry.Builder().offset(tis.getCurrentOffset())
.size((int) te.getSize()).build();
tarEntryMap.put(entryName, h5TarEntry);
}
tis.close();
3、讀取指定entryName的數據塊
public synchronized static byte[] get(String appId, String entryName) {
try {
byte buffer[] = new byte[2048];
int count;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (!tarEntryMap.containsKey()) {
return null;
}
long offset = tarEntryMap.get(entryName).getOffset();
int entrySize = tarEntryMap.get(entryName).getSize();
FileInputStream fis = new FileInputStream(tarPath);
BufferedInputStream bis = new BufferedInputStream(fis);
TarInputStream tis = new TarInputStream(bis);
H5Log.d(TAG, "entryName" + entryName + " skip offset:" + offset + " size" + entrySize);
tis.skip(offset);
if (buffer.length > entrySize) {
buffer = new byte[entrySize];
}
int bufferSize = 0;
while ((count = tis.read(buffer)) != -1) {
bos.write(buffer, 0, count);
bufferSize += count;
// 當前buffer加上已經讀取的bufferSize如果超過entrySize那麼我們就應該重新計算buffer進行最後一次讀取。
if ((bufferSize + buffer.length) > entrySize) {
buffer = new byte[entrySize % bufferSize];
bufferSize = entrySize - entrySize % bufferSize;
}
if (buffer.length == entrySize || entrySize == bufferSize) {
break;
}
}
tis.close();
byte[] data = bos.toByteArray();
if (data == null) {
return null;
}
H5Log.d(TAG, "entryName:" + entryName);
return data;
} catch (IOException e) {
H5Log.e(TAG, "exception :" + e);
}
return null;
}
這樣就可以通過指定的entryName,根據其offset和 size 計算到這個entry在TarStream中固定內存塊,從而拿到真正的數據。
兩種讀取方式的區別:
第一種
優點:減少了I/O操作。
缺點:耗費了內存。假如一個很大的資源在這個tar中,但是被使用的概率很低,這樣耗費了內存從而不值得這麼做。
第二種
優點:節省了內存,提高了讀取效率
缺點:增加了I/O操作,Tar資源可能存在被篡改的風險。
Thanks.
By MC.