一文读懂resource.arsc文件结构

概述

    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所对应的数组,然后找到其中的第0X013c号元素。这个元素恰好就是"img => ./res/drawable-xxhdpi/img.png",左边是资源名称,右边是资源的文件路径,有了这个字符串程序便可以访问到对应的资源文件了。

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

chunk

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

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

arsc文件结构

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

image

    图片整体描述了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均由一个头结构开始,在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。

所以头部结构如下:


image

StringPool

    接着是字符串资源池chunk,它的结构如下图:

image

    字符串常量池存放了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与这两个字符串常量池的位置关系:

image

    加上之前的字符串常量池,在整个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指向具体的资源,两种资源类型的具体存储方式如下图所示:

image

最后再一起了解一下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 文件格式解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342