概述
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.xxx
、R.layout.xxx
、R.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
文件结构的网络图片。
图片整体描述了arsc
文件中各个chunk
的关系(注意结合图片左右两侧内容):
- 整个
arsc
文件是一个RES_TABLE_TYPE
类型的chunk
;RES_TABLE_TYPE
可分为三个部分:文件头部和两个子chunk
(RES_STRING_POOL_TYPE
、RES_TABLE_PACKAGE_TYPE
);RES_TABLE_PACKAGE_TYPE
中包含了:头部、资源类型字符串常量池、资源项名称字符串常量池、多个子chunk
(RES_TABLE_TYPE_SPEC_TYPE
和RES_TABLE_TYPE_TYPE
);- 每种类型的
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
类型,其中type
为RES_TABLE_TYPE
packageCount
:arsc
文件中ResTablePackage
的个数,通常是 1。
所以头部结构如下:
StringPool
接着是字符串资源池chunk
,它的结构如下图:
字符串常量池存放了APK中所有的字符串资源的内容,这个
chunk
由图中的五个部分组成:
-
ResStringPool_header
: 字符串常量池常量头部 -
String Offset Array
: 字符串偏移数组,数组中的每个元素记录一条字符串在此常量池中的起始位置的偏移量,没个偏移量大小为4字节,所以此区域的大小为(4 xstringCount
)字节 -
Style Offset Array
: 字符串样式偏移数组 -
String Content
: 字符串常量池内容区域,池中的每个字符串元素末尾含有一个字符串结束符 -
Style Content
: 字符串样式内容区域
我们主要关心:ResStringPool_header
、String Offset Array
和String 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
,其中type
是RES_STRING_POOL_TYPE
stringCount
: 常量池中的字符串个数styleCount
: 常量池中字符串样式个数flags
: 等于0、SORTED_FLAG
、UTF8_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
, 其type
是RES_TABLE_PACKAGE_TYPE
id
: 包的ID
, 等于 Package Id,一般用户包的Package Id
为0X7F
, 系统资源包的Package Id
为0X01
。name
: 包名typeStrings
:资源类型字符串资源池相对头部的偏移位置lastPublicType
: 类型字符串资源池的大小keyStrings
: 资源项字符串相对头部的偏移位置lastPublicKey
: 一资源项名称字符串资源池的大小typeIdOffset
: 未知,值为 0
上述结构中的typeStrings
、keyStrings
中,提到了资源类型字符串常量池与资源项名称常量池,这两个字符串常量池的结构也是ResStringPool
,他们的位置紧随ResTable_package
之后,分别是Type String Pool
与Type String Pool
。通过下图可以看到ResTable_package
与这两个字符串常量池的位置关系:
加上之前的字符串常量池,在整个
arsc
文件中一共有三个字符串常量池:字符串资源常量池、资源类型字符串常量池、资源项名称字符串常量池。
比如:
hello world
表示一个资源类型为
string
,名字为tip
,值为hello world
的资源。
hello world
为 字符串资源,存储在 字符串资源 常量池中;string
为 资源类型 ,存储在 资源类型 字符串常量池中;tip
为 资源项名称 ,存储在 资源项名称 字符串常量池中;当资源为
R.drawable.img
时,资源类型为drawable
、资源项名称为img
、R.drawable.img
资源所对应的文件路径存储则在 字符串资源 中。
ResTable_typeSpec
和 ResTable_type
文章开头说讲arsc
是一个由多个Pair[]
组成的文件,每种资源类型(anim
、attr
、drawable
、string
等)对应一个Pair[]
,这个Pair[]
就是接下来要讲到的ResTable_typeSpec
和 ResTable_type
。
实际上在arsc
文件中,每种资源类型对应一个ResTable_typeSpec
,它用来描述资源项的配置差异性,每个ResTable_typeSpec
由 头部、一个或多个 ResTable_type
组成,ResTable_type
的数量由适配类型数目决定,例如:drawable、drawable-mdpi、drawable-hdpi等每种适配类型对应一个ResTable_type
。而每个ResTable_type
则由一个 头部 和一个 资源项数组 构成,这个资源项数组就是上面提到的Pair[]
。
以drawable
的ResTable_typeSpec
和ResTable_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_typeSpec
和ResTable_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
res0
、res1
:保留字段,值为0entryCount
: 本类型的资源项个数,注意,这里是指名称相同的资源项的个数
资源类型的分析完成后,我们再看看适配类型所用的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
: 保留字段,值为0entryCount
:本类型的资源项个数,注意,这里是指名称相同的资源项的个数。entriesStart
:资源项数据块相对本chunk
头部的偏移值。config
:指向一个ResTable_config,用来描述配置信息(用以区别Type
是何种适配类型)
紧随其后的是资源项池(一个资源项数组)到底是如何存储具体的资源的.
资源项池中的资源项的存储方式有两种,分别如下:
- 普通资源 :
ResTable_entry
+Res_value
bag
资源 :ResTable_entry
+ResTable_map_entry
+Res_Table_map
* n
其中, ResTable_entry
指向资源项名称,并标识此资源是否为一个bag
资源; Res_value
、Res_Table_map
指向具体的资源,两种资源类型的具体存储方式如下图所示:
最后再一起了解一下ResTable_entry
、Res_value
、ResTable_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
: 保留字段,值为0dataType
: 当前数据的类型,这个为枚举类型(string、dimension等),具体可以查看ResourceTypes.hdata
: 数据。根据上面的数据类型定,如果类型为string,则当前的值为字符串资源池中的索引
最后看看bag
资源的存储结构的具体内容(显然bag
资源的存储结构已经不满足我们上述说的 Pair
对象,Pair
对象的引入只是帮助我们理解非bag
资源的存储结构),ResTable_map_entry
与ResTable_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 文件格式解析