一文读懂resource.arsc文件结构

5,883 阅读13分钟

概述


    resource.arsc文件是Apk打包过程中的产生的一个资源索引文件。在对apk进行解压或者使用Android Studio对apk进行分析时便可以看到resource.arsc文件。

    通过学习resource.arsc文件结构,可以帮助我们深入了解apk包体积优化中使用到的 重复资源删除资源文件名混淆 技术。

arsc文件作用


    在java中访问一个文件是需要提供文件的文件名,例如:

    new File("./res/drawable-xxhdpi/img.png");

    然而在Android中,却可以通过drawable Id获得资源文件:

    getDrawable(R.drawable.img);

    这里凭一个id就能获取资源文件内容,省去了文件路径的手动输入,其背后就是通过读取arsc文件实现的。

    这些R.drawable.xxxR.layout.xxxR.string.xxx等的值(存储在R.jar或者R.java文件中)称之为 资源索引 ,通过这些资源索引可以在arsc文件中查取实际的资源路径或资源值;

    例如: getDrawable(R.drawable.img)在编译后成了getDrawable(2131099964),再将id转为十六进制:

2131099964 = 0x7f06013c

    这时的资源索引为0x7f06013c

    资源索引具有固定的格式:0xPPTTEEEE

PackageId(2位) + TypeId(2位) + EntryId(4位)

  • PP:Package ID,包的命名空间,取值范围为[0x01, 0x7f],第三方应用均为7f。

  • TT:资源类型,有anim、layout、mipmap、string、style等资源类型。

  • EEEE:代表某一类资源在偏移数组中的值

    所以,0x7f06013c中 PackageId = 0x7f、TypeId = 0x06、EntryId = 0x013c

    最简单的我们可以将arsc函数想象成一个含有多个Pair数组的文件,且每个资源类型(TypeId)对应一个Pair[](或多个,为了便于理解先只认为是一个)。因此在arsc中查找0x7f06013c元素的值,就是去设法找到TypeId=0x06所对应的数组Pair[],然后找到其中的第0X013c号元素Pair[0X013c]。这个元素恰好就是Pair("img", "./res/drawable-xxhdpi/img.png"),左边是资源名称img,右边是资源的文件路径"./res/drawable-xxhdpi/img.png",有了文件路径,程序便可以访问到对应的资源文件了。

    当然实际的arsc文件在结构上要稍微复杂一点,下面开始分析arsc文件结构。

chunk


    为了便于理解,在正式介绍resource.arsc(以下简称arsc)文件前,需要对chunk进行解释一下,在其他文章中也多次使用了“chunk”这个词。

    chunk翻译为中文就是“块、部分(尤指大部分,一大块)”的意思,例如:一棵树,可以分为三个chunk(部分):树冠、树茎、树根。也可以将一棵树视为一个chunk,这个chunk就是这棵树。

arsc文件结构


    resources.arsc是一个二进制文件,其内部结构的定义在ResourceTypes.h,不喜欢这个文件的同学,可以先看这张描述arsc文件结构的网络图片。

    图片整体描述了arsc文件中各个chunk的关系(注意结合图片左右两侧内容):

  1. 整个arsc文件是一个 RES_TABLE_TYPE 类型的chunk
  2. RES_TABLE_TYPE 可分为三个部分:文件头部和两个子chunk( RES_STRING_POOL_TYPERES_TABLE_PACKAGE_TYPE );
  3. RES_TABLE_PACKAGE_TYPE 中包含了:头部、资源类型字符串常量池、资源项名称字符串常量池、多个子chunk(RES_TABLE_TYPE_SPEC_TYPERES_TABLE_TYPE_TYPE );
  4. 每种类型的chunk都含有一个头结构

    arsc文件的结构大致可以用如下的伪代码表示:

//---------------------------------------------------------------------------
//: arsc文件是一个 RES_TABLE_TYPE 类型的chunk
RES_TABLE_TYPE {
    table_header//文件头部
    RES_STRING_POOL_TYPE //常量池chunk
    RES_TABLE_PACKAGE_TYPE//内容chunk
}
//---------------------------------------------------------------------------
//:字符串常量池chunk
RES_STRING_POOL_TYPE {
    pool_header//字符串常量池头部
    string[] //常量池
}
//---------------------------------------------------------------------------
//: 内容chunk
RES_TABLE_PACKAGE_TYPE {
    package_header//chunk头部
    RES_STRING_POOL_TYPE//资源类型字符串常量池,类型为:RES_STRING_POOL_TYPE,内容为:[anim,attr,bool,color,dimen,drawable,id,integer,interpolator,layout,mipmap,string,style]
    RES_STRING_POOL_TYPE//资源项名称字符串常量池
    //资源类型chunk:在上述的ResTypeName_StringPool(资源类型常量池)中的每一个类型都有一个资源类型的chunk。这里以drawable为例
    //drawable资源类型chunk
    RES_TABLE_TYPE_SPEC_TYPE{
        spec_header//spec头部
        //drawable-mdpi
        RES_TABLE_TYPE_TYPE
        //drawable-hdpi
        RES_TABLE_TYPE_TYPE
        ...
    }
    //attr资源类型chunk
    RES_TABLE_TYPE_SPEC_TYPE{
        RES_TABLE_TYPE_TYPE
        RES_TABLE_TYPE_TYPE{
            type_header//type头部
            //具体的资源项池:资源名:资源值
            ResName:ResValue
            ResName:ResValue
            ResName:ResValue
            ResName:ResTableMapEntry->[Res_value1, Res_value2]
            ResName:ResTableMapEntry->->[Res_value1, Res_value2,Res_value3]
        }
        ...
    }
    ...
    ...
}
//---------------------------------------------------------------------------

Chunk头结构

    上述说到每一种chunk均由一个对应头结构开始,而且这些头结构均以ResChunk_header开始,在ResourceTypes.h中,这个头结构被定义为ResChunk_header

/**
 * Header that appears at the front of every data chunk in a resource.
 */
struct ResChunk_header
{
	
    // Type identifier for this chunk.  The meaning of this value depends
    // on the containing chunk.
    uint16_t type;

    // Size of the chunk header (in bytes).  Adding this value to
    // the address of the chunk allows you to find its associated data
    // (if any).
    uint16_t headerSize;

    // Total size of this chunk (in bytes).  This is the chunkSize plus
    // the size of any data associated with the chunk.  Adding this value
    // to the chunk allows you to completely skip its contents (including
    // any child chunks).  If this value is the same as chunkSize, there is
    // no data associated with the chunk.
    uint32_t size;
};

uint16_t: 16位无符号整形(2字节)、uint32_t:32位无符号整形(4字节)

结构分析

  • type : chunk块的类型,部分定义如下:
enum {
   RES_NULL_TYPE               = 0x0000,
   RES_STRING_POOL_TYPE        = 0x0001,
   RES_TABLE_TYPE              = 0x0002,
   // Chunk types in RES_TABLE_TYPE
   RES_TABLE_PACKAGE_TYPE      = 0x0200,
   RES_TABLE_TYPE_TYPE         = 0x0201,
   RES_TABLE_TYPE_SPEC_TYPE    = 0x0202,
   RES_TABLE_LIBRARY_TYPE      = 0x0203
};
  • headerSize : chunk头部大小
  • size : 所在chunk块的大小

ResTable_header

    首先,文件头部是一个ResTable_header结构:

struct ResTable_header
{
    struct ResChunk_header header;

    // The number of ResTable_package structures.
    uint32_t packageCount;
};

    结构分析:

  • header : ResChunk_header类型,其中typeRES_TABLE_TYPE
  • packageCount : arsc文件中ResTablePackage的个数,通常是 1。

所以头部结构如下:

StringPool

    接着是字符串资源池chunk,它的结构如下图:     字符串常量池存放了APK中所有的字符串资源的内容,这个chunk由图中的五个部分组成:

  • ResStringPool_header : 字符串常量池常量头部
  • String Offset Array : 字符串偏移数组,数组中的每个元素记录一条字符串在此常量池中的起始位置的偏移量,没个偏移量大小为4字节,所以此区域的大小为(4 x stringCount)字节
  • Style Offset Array : 字符串样式偏移数组
  • String Content : 字符串常量池内容区域,池中的每个字符串元素末尾含有一个字符串结束符
  • Style Content : 字符串样式内容区域

我们主要关心:ResStringPool_headerString Offset ArrayString Content

首先分析字符串常量池的头部,这个头部是一个ResStringPool_header结构:

struct ResStringPool_header
{
    struct ResChunk_header header;

    // Number of strings in this pool (number of uint32_t indices that follow
    // in the data).
    uint32_t stringCount;

    // Number of style span arrays in the pool (number of uint32_t indices
    // follow the string indices).
    uint32_t styleCount;

    // Flags.
    enum {
        // If set, the string index is sorted by the string values (based
        // on strcmp16()).
        SORTED_FLAG = 1<<0,

        // String pool is encoded in UTF-8
        UTF8_FLAG = 1<<8
    };
    uint32_t flags;

    // Index from header of the string data.
    uint32_t stringsStart;

    // Index from header of the style data.
    uint32_t stylesStart;
};

结构分析:

  • header : ResChunkHeader,其中typeRES_STRING_POOL_TYPE
  • stringCount : 常量池中的字符串个数
  • styleCount : 常量池中字符串样式个数
  • flags : 等于0、SORTED_FLAGUTF8_FLAG或者它们的组合值,用来描述字符串资源串的属性,例如,SORTED_FLAG位等于1表示字符串是经过排序的,而UTF8_FLAG位等于1表示字符串是使用UTF8编码的,否则就是UTF16编码的
  • stringsStart : 字符串内容与常量池头部起始点之间的偏移距离
  • stylesStart : 字符串样式内容与常量池头部起始点之间的偏移距离

Package

    最后,分析Package,这个chunk以一个ResTable_package结构开始:

/**
 * A collection of resource data types within a package.  Followed by
 * one or more ResTable_type and ResTable_typeSpec structures containing the
 * entry values for each resource type.
 */
struct ResTable_package
{
    struct ResChunk_header header;
    
    // If this is a base package, its ID.  Package IDs start
    // at 1 (corresponding to the value of the package bits in a
    // resource identifier).  0 means this is not a base package.
    uint32_t id;

    // Actual name of this package, \0-terminated.
    uint16_t name[128];

    // Offset to a ResStringPool_header defining the resource
    // type symbol table.  If zero, this package is inheriting from
    // another base package (overriding specific values in it).
    uint32_t typeStrings;

    // Last index into typeStrings that is for public use by others.
    uint32_t lastPublicType;

    // Offset to a ResStringPool_header defining the resource
    // key symbol table.  If zero, this package is inheriting from
    // another base package (overriding specific values in it).
    uint32_t keyStrings;

    // Last index into keyStrings that is for public use by others.
    uint32_t lastPublicKey;

    uint32_t typeIdOffset;
};

    结构分析:

  • header : 类型为ResChunk_header , 其typeRES_TABLE_PACKAGE_TYPE
  • id : 包的ID, 等于 Package Id,一般用户包的Package Id0X7F, 系统资源包的 Package Id0X01
  • name : 包名
  • typeStrings :资源类型字符串资源池相对头部的偏移位置
  • lastPublicType : 类型字符串资源池的大小
  • keyStrings : 资源项字符串相对头部的偏移位置
  • lastPublicKey : 一资源项名称字符串资源池的大小
  • typeIdOffset : 未知,值为 0

    上述结构中的typeStringskeyStrings中,提到了资源类型字符串常量池与资源项名称常量池,这两个字符串常量池的结构也是ResStringPool,他们的位置紧随ResTable_package之后,分别是Type String PoolType String Pool。通过下图可以看到ResTable_package与这两个字符串常量池的位置关系:     加上之前的字符串常量池,在整个arsc文件中一共有三个字符串常量池:字符串资源常量池、资源类型字符串常量池、资源项名称字符串常量池。

比如:

<string name="tip">hello world</string>

表示一个资源类型为string,名字为tip,值为hello world的资源。

  • hello world字符串资源,存储在 字符串资源 常量池中;
  • string资源类型 ,存储在 资源类型 字符串常量池中;
  • tip资源项名称 ,存储在 资源项名称 字符串常量池中;

当资源为R.drawable.img时,资源类型为drawable、资源项名称为imgR.drawable.img资源所对应的文件路径存储则在 字符串资源 中。

ResTable_typeSpecResTable_type

    文章开头说讲arsc是一个由多个Pair[]组成的文件,每种资源类型(animattrdrawablestring等)对应一个Pair[],这个Pair[]就是接下来要讲到的ResTable_typeSpecResTable_type

    实际上在arsc文件中,每种资源类型对应一个ResTable_typeSpec,它用来描述资源项的配置差异性,每个ResTable_typeSpec头部、一个或多个 ResTable_type 组成,ResTable_type的数量由适配类型数目决定,例如:drawable、drawable-mdpi、drawable-hdpi等每种适配类型对应一个ResTable_type。而每个ResTable_type则由一个 头部 和一个 资源项数组 构成,这个资源项数组就是上面提到的Pair[]

    以drawableResTable_typeSpecResTable_type的结构为例,可以表示成如下结构:

//drawable
RES_TABLE_TYPE_SPEC_TYPE{
	//drawable-mdpi
	RES_TABLE_TYPE_TYPE
	//drawable-hdpi
	RES_TABLE_TYPE_TYPE{
		ResChunk_header//type头部
		//具体的资源项数组:资源名->资源值
		ResName->ResValue
		ResName->ResValue
		ResName->ResValue
		//ResName->ResTableMapEntry
		//ResName->ResTableMapEntry
        ...
	}
	...
}

    那arsc文件中ResTable_typeSpecResTable_type具体是怎么表示的呢?

    首先看ResTable_typeSpec类型:

struct ResTable_typeSpec
{
    struct ResChunk_header header;

    // The type identifier this chunk is holding.  Type IDs start
    // at 1 (corresponding to the value of the type bits in a
    // resource identifier).  0 is invalid.
    uint8_t id;
    
    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;
    
    // Number of uint32_t entry configuration masks that follow.
    uint32_t entryCount;

    enum : uint32_t {
        // Additional flag indicating an entry is public.
        SPEC_PUBLIC = 0x40000000u,

        // Additional flag indicating an entry is overlayable at runtime.
        // Added in Android-P.
        SPEC_OVERLAYABLE = 0x80000000u,
    };
};

    结构分析:

  • header: 头部,type等于RES_TABLE_TYPE_SPEC_TYPE
  • id : 表示资源类型id,通过这个id可以在资源类型常量池中获取资源类型,这个id就是0xPPTTEEEE中的TT
  • res0res1:保留字段,值为0
  • entryCount : 本类型的资源项个数,注意,这里是指名称相同的资源项的个数

    资源类型的分析完成后,我们再看看适配类型所用的ResTable_type以及具体的资源项。

    依然是从其头部开始分析:

struct ResTable_type
{
    struct ResChunk_header header;

    enum {
        NO_ENTRY = 0xFFFFFFFF
    };
    
    // The type identifier this chunk is holding.  Type IDs start
    // at 1 (corresponding to the value of the type bits in a
    // resource identifier).  0 is invalid.
    uint8_t id;
    
    enum {
        // If set, the entry is sparse, and encodes both the entry ID and offset into each entry,
        // and a binary search is used to find the key. Only available on platforms >= O.
        // Mark any types that use this with a v26 qualifier to prevent runtime issues on older
        // platforms.
        FLAG_SPARSE = 0x01,
    };
    uint8_t flags;

    // Must be 0.
    uint16_t reserved;
    
    // Number of uint32_t entry indices that follow.
    uint32_t entryCount;

    // Offset from header where ResTable_entry data starts.
    uint32_t entriesStart;

    // Configuration this collection of entries is designed for. This must always be last.
    ResTable_config config;
};

    结构分析:

  • header : ResChunk_header类型,其中type等于RES_TABLE_TYPE_TYPE
  • reserved : 保留字段,值为0
  • entryCount :本类型的资源项个数,注意,这里是指名称相同的资源项的个数。
  • entriesStart:资源项数据块相对本chunk头部的偏移值。
  • config:指向一个ResTable_config,用来描述配置信息(用以区别Type是何种适配类型)

    紧随其后的是资源项池(一个资源项数组)到底是如何存储具体的资源的.

    资源项池中的资源项的存储方式有两种,分别如下:

  • 普通资源 : ResTable_entry + Res_value
  • bag资源 :ResTable_entry + ResTable_map_entry + Res_Table_map * n

    其中, ResTable_entry指向资源项名称,并标识此资源是否为一个bag资源; Res_valueRes_Table_map指向具体的资源,两种资源类型的具体存储方式如下图所示:

    最后再一起了解一下ResTable_entryRes_valueResTable_map_entry的内部结构。

    先看ResTable_entry

struct ResTable_entry
{
    // Number of bytes in this structure.
    uint16_t size;

    enum {
        // If set, this is a complex entry, holding a set of name/value
        // mappings.  It is followed by an array of ResTable_map structures.
        FLAG_COMPLEX = 0x0001,
        // If set, this resource has been declared public, so libraries
        // are allowed to reference it.
        FLAG_PUBLIC = 0x0002,
        // If set, this is a weak resource and may be overriden by strong
        // resources of the same name/type. This is only useful during
        // linking with other resource tables.
        FLAG_WEAK = 0x0004
    };
    uint16_t flags;
    
    // Reference into ResTable_package::keyStrings identifying this entry.
    struct ResStringPool_ref key;
};

    结构分析:

  • size:资源项头部大小。
  • flags:资源项标志位。flags = FLAG_COMPLEX表示此资源为Bag资源项,并且在ResTable_entry后紧随ResTable_map数组表示资源项内容,否则的话,在ResTable_entry后紧随Res_value : 资源项内容。如果是一个可以被引用的资源项,那么FLAG_PUBLIC位就等于1。
  • key:__资源项名称__在资源项名称字符串资源池的索引。

    资源项名称在ResTable_entry中已经找到了,接着看资源值Res_Value:

struct Res_value
{
    // Number of bytes in this structure.
    uint16_t size;
    
    // Always set to 0.
    uint8_t res0;
    
    uint8_t dataType;
    // The data for this item, as interpreted according to dataType.
    typedef uint32_t data_type;
    
    data_type data;
};

    结构分析:

  • size: Res_value的大小
  • res0: 保留字段,值为0
  • dataType : 当前数据的类型,这个为枚举类型(string、dimension等),具体可以查看ResourceTypes.h
  • data : 数据。根据上面的数据类型定,如果类型为string,则当前的值为字符串资源池中的索引

    最后看看bag资源的存储结构的具体内容(显然bag资源的存储结构已经不满足我们上述说的 Pair对象,Pair对象的引入只是帮助我们理解非bag资源的存储结构),ResTable_map_entryResTable_ref

struct ResTable_map_entry : public ResTable_entry
{
    // Resource identifier of the parent mapping, or 0 if there is none.
    //父ResTable_map_entry的资源ID,如果没有父ResTable_map_entry,则等于0
    ResTable_ref parent;
    // Number of name/value pairs that follow for FLAG_COMPLEX.
    //bag项的个数
    uint32_t count;
};

struct ResTable_map
{
	//bag的资源项ID
    ResTable_ref name;
    
    // This mapping's value.
    //bag的资源项值
    Res_value value;
};

struct ResTable_ref
{
    uint32_t ident;
};

    至此,已完成resource.arsc问价的分析。

结尾

    arsc文件的结构总体并不算非常复杂,android-chunk-utils是一个用java编写的arsc文件解析工具,通过该工具可以帮助理解arsc文件的结构,同时通过该工具也可以更改arsc文件内容,完成资源文件名混淆与重复资源优化等。

推荐阅读

1 、ResourceTypes.h

2、Android应用程序资源的编译和打包过程分析

3、Android 手把手分析resources.arsc

4、Android 逆向笔记 —— ARSC 文件格式解析