Tar包高优化解析

本文将介绍一个针对Tar包解析时的优化方案,旨在优化内存、提高效率。

一、首先讲一个tar包的文件结构。(懂得可以绕开此段)

tar只是一个归档文件,并不进行压缩。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  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当作八个空格,即80x20)
  文件内容以512字节为一个block进行分割,最后一个block不足部分以0补齐
  两个文件的tar包首先存放第一个文件的tar头结构,然后存储文件内容,接着存储第二个文件的tar头结构,然后存储文件内容
  所有文件都存储完了以后,最后存放一个全零的tar结构
  所有的tar文件大小应该都是512的倍数,一个空文件打包后为512
3字节,包括一个tar结构头,一个全零的block存储文件内容,一个全零的tar结构

  • 检测tar文件格式的方法:
    1
    2
    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集合中.

三、内存优化的 “解压”方式

>
由于每一个TarEntry都是一个固定大小字节的对象,那么我们可不可以直接读取这块内存,而不是将所有都常驻内存呢?
答案当然是可以的。
为了内存上的优化和效率上的提升,我们可以直接读取指定EntryNam的内存块。
因为一个tar包基本的组成结构就是 entryName->data。我们可以拿到每一个EntryName和其对应的内存大小、偏移量,在读取的时候直接在TarInputStream中读取相应内存块。
代码如下:

  1. 一个简单维护TarEntry偏移量和字节大小的类McTarEntry。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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的数据块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    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.