Tar包解析的內存優化方案

  本文將介紹一個針對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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章