一、前言
对于APK里面的Resources.arsc文件大家应该都知道是干什么的(不知道的请看我的另一篇文章Android应用程序资源文件的编译和打包原理),它实际上就是App的资源索引表。下面我会结合实例对它的格式做一下剖析,读完这篇文章应该能够知道Resources.arsc的格式,并可以从二进制的文件中查找到资源的相关信息,或者根据资源的id可以定位到二进制文件中的位置。不过本人对Android资源文件的有一些相关概念并不是特别熟悉,所以文章中有很多地方也并不明白,如有错误欢迎指正!
二、R.java文件及资源ID
首先先介绍一下我们在Android应用开发过程中程序中用的资源的id,相信大家都知道R.java文件,这个是通过aapt对资源文件进行编译生成的资源id文件,这样我们程序中使用资源文件更加方便。举例我们先看一下原始的资源文件res/values/strings.xml内容如下:
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <string name="app_name">Cert</string>
- <string name="hello_world">Hello world!</string>
- <string name="action_settings">Settings</string>
- </resources>
这里先介绍几个概念,上面的app_name和hello_world这些叫做资源项名称(其它的还有windowActionBar、ActionBarTabStyle类似这种),而它们对应的资源项类型就是string(其它的还有attr、drawable类似这些),资源项的值就是Cert和Hello world!这些。
下面是对应R.java文件的内容:
- public final class R {
- ...
- public static final class string {
- ...
- /** Description of the choose target button in a ShareActionProvider (share UI). [CHAR LIMIT=NONE]
- */
- public static final int abc_shareactionprovider_share_with=0x7f0a000c;
- /** Description of a share target (both in the list of such or the default share button) in a ShareActionProvider (share UI). [CHAR LIMIT=NONE]
- */
- public static final int abc_shareactionprovider_share_with_application=0x7f0a000b;
- public static final int action_settings=0x7f0a000f;
- public static final int app_name=0x7f0a000d;
- public static final int hello_world=0x7f0a000e;
- }
- ...
- }
代码段2
可以看到每个资源文件在R中都是一个class,每个资源项名称都分配了一个id,id值是一个四字节无符号整数,格式是这样的:0xpptteeee,(p代表的是package,t代表的是type,e代表的是entry),最高字节代表Package ID,次高字节代表Type ID,后面两个字节代表Entry ID。
Package ID相当于是一个命名空间,限定资源的来源。Android系统当前定义了两个资源命令空间,其中一个系统资源命令空间,它的Package ID等于0x01,另外一个是应用程序资源命令空间,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之间的Package ID都是合法的,而在这个范围之外的都是非法的Package ID。前面提到的系统资源包package-export.apk的Package ID就等于0x01,而我们在应用程序中定义的资源的Package ID的值都等于0x7f,这一点可以通过生成的R.java文件来验证。
Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。
Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。
三、解析Resources.arsc
1. Resources.arsc文件格式
下面我们开始看Resources.arsc(后面截图给出的resources.arsc文件的二进制内容都是与上面代码段1和代码段2相对应的),首先看一下文件的格式,如下面两个图:
图2
以上两个图都是Resources.arsc文件的格式,图1是从网上找的,其中很多项都展开了,不了解对应的数据结构肯定看不懂,所以我自己画了图2(画图好蛋疼的说~),相对来说更容易接受一点,这里都放出来做个对照吧。Resources.arsc对应的数据结构的定义在Android源码/frameworks/base/include/androidfw/ResourceType.h中,大家可以自己去看一下。
2. chunk
下面我来从上到下介绍一下文件的格式,首先是chunk概念,整个文件是由一系列的chunk构成的,算是整个文件划分的基本单位吧,实际上就是把整个文件无差别的划分成多个模块,每个模块就是一个chunk,结构更加清晰。每个chunk是最前面是一个ResChunk_header的结构体,描述这个chunk的信息,ResChunk_header如下:
- struct ResChunk_header
- {
- enum
- {
- RES_NULL_TYPE = 0x0000,
- RES_STRING_POOL_TYPE = 0x0001,
- RES_TABLE_TYPE = 0x0002,
- RES_XML_TYPE = 0x0003,
- RES_XML_FIRST_CHUNK_TYPE = 0x0100,
- RES_XML_START_NAMESPACE_TYPE= 0x0100,
- RES_XML_END_NAMESPACE_TYPE = 0x0101,
- RES_XML_START_ELEMENT_TYPE = 0x0102,
- RES_XML_END_ELEMENT_TYPE = 0x0103,
- RES_XML_CDATA_TYPE = 0x0104,
- RES_XML_LAST_CHUNK_TYPE = 0x017f,
- RES_XML_RESOURCE_MAP_TYPE = 0x0180,
- RES_TABLE_PACKAGE_TYPE = 0x0200,
- RES_TABLE_TYPE_TYPE = 0x0201,
- RES_TABLE_TYPE_SPEC_TYPE = 0x0202
- };
- //当前这个chunk的类型
- uint16_t type;
- //当前这个chunk的头部大小
- uint16_t headerSize;
- //当前这个chunk的大小
- uint32_t size;
- };
3. 文件header
Resources.arsc文件的最开始是整个文件的header,结构是ResTable_header:
- struct ResTable_header
- {
- struct ResChunk_header header;
- // The number of ResTable_package structures.
- uint32_t packageCount;
- ;
可以看到header就是一个chunk,以ResChunk_header结构开头来描述这个chunk。resources.arsc文件的header内容如下图中选中部分:
图3
图中选中的部分就是header,可以看到类型是0x0002,对应类型是RES_TABLE_TYPE,headerSize是0x0c,整个chunk的大小也就是文件的大小是0x019584,package的数量是1个。
4. 全局字符串池
紧接着是Global String Pool,全局字符串池,这也是Resources.arsc存在最重要的一个原因之一,就是把所有字符串放到这个池子里,大家复用这些字符串,可以很大的减小APK包的尺寸。从图1和图2可以看到后面还有两个字符串池,那么什么字符串会放到这个全局字符串池中呢?所有的资源文件的路径名,以及资源文件中所定义的资源的值,比如代码段1中的Cert和Hello world!都存在这里。
字符串池的结构体如下:
- 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;<span style="white-space:pre"> </span>//If flags is 0x0, string pool is encoded in UTF-16
- // Index from header of the string data.
- uint32_t stringsStart;
- // Index from header of the style data.
- uint32_t stylesStart;
对应的二进制内容如下图选中部分:
图4
从图中可以看到类型是0x0001,对应代码段3中RES_STRING_POOL_TYPE,整个chunk的大小是0x919C,stringCount是0x03E1,styleCount是0,flags是0x0100即UTF8格式,stringsStart即字符串相对头部起始位置的偏移是0x0FA0。
从图2中可以看到紧接着header的是stringCount个字符串偏移数组,数组每一个元素记录着每个字符串的起始位置相对于stringsStart的偏移。字符串池中每个UTF8格式字符串都是以字符串结束符0x00结束的,UTF16是0x0000。
style偏移数组与string是一样的就不多说了,但这个style是干什么的现在我还不清楚,以后知道了再更新。
5. Package解析
下面要介绍重头戏Package了。首先是一个package的header,结构体如下:
- struct ResTable_package
- {
- struct ResChunk_header header;
- //包的ID,等于Package Id,一般用户包的值Package Id为0X7F,系统资源包的Package Id为0X01。
- uint32_t id;
- //包名称
- char16_t name[128];
- //类型字符串资源池相对头部的偏移
- uint32_t typeStrings;
- //最后一个导出的Public类型字符串在类型字符串资源池中的索引,目前这个值设置为类型字符串资源池的元素个数。
- uint32_t lastPublicType;
- //资源项名称字符串相对头部的偏移
- uint32_t keyStrings;
- //最后一个导出的Public资源项名称字符串在资源项名称字符串资源池中的索引,目前这个值设置为资源项名称字符串资源池的元素个数。
- uint32_t lastPublicKey;
- };
图4中全局字符串池的起始位置是0xC,而整个chunk的大小是0x919C,那么package的起始位置就是两者相加得到0x91A8,对应二进制内容如下图选中部分:
图5
从上图可以看到chunk类型是0x0200,对应代码段3中的RES_TABLE_PACKAGE_TYPE,id是0x7F(这与R.java中的每个资源id的最高字节是一样的),这个package的名字是com.example.cert,类型字符串池typeStrings相对于package header起始位置的偏移是0x011C,类型字符串的个数是0x0C,资源项名称字符串池keyStrings相对于package header起始位置的偏移是0x01C8,个数是0x01E1。
5.1 类型字符串池和资源项名称字符串池
对于类型字符串池(图2中的Type String Pool)和资源项名称字符串池(图2中的Key String Pool)的结构和内容我这里就不贴出来了,结构和全局字符串池是一样的。类型字符串池中存储的是所有类型相关的字符串,比如attr,drawable,layout这些;而资源项名称字符串池中存储的是应用所有资源文件中的资源项名称相关的字符串,比如代码段1中的app_name,hello_world,action_settings。
5.2 类型规范数据块(Type Spec)
类型规范数据块用来描述资源项的配置差异性。通过这个差异性描述,我们就可以知道每一个资源项的配置状况。知道了一个资源项的配置状况之后,Android资源管理框架在检测到设备的配置信息发生变化之后,就可以知道是否需要重新加载该资源项。类型规范数据块是按照类型来组织的,也就是说,每一种类型都对应有一个类型规范数据块。
上面是从参考文章里copy过来的,可能有些人不太了解这个Type Spec是什么东西,我个人的理解它实际上就是类型。说到这里需要提几句Android资源文件的配置问题,大家都知道Android设备众多,为了使得一个应用程序能够在运行时同时支持不同的大小和密度的屏幕,以及支持国际化,即支持不同的国家地区和语言,Android应用程序资源的组织方式有18个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在UI上,从而提高用户体验。也就是说,每一个资源类,都会有一个配置列表,配置着这个资源类的不同维度的信息,那么Type Spec就是这个资源类的代表。比如前面看到的attr,drawable,string这种都是资源类,Type Spec就是描述这些的结构,前面说到过R.java中每个资源id的格式是0xpptteeee,里面那个次高字节的tt就是Type Spec的id,同时这个id值也是这个Type Spec的类型名称在Type String Pool类型字符串池中索引数组的索引值,根据id值就可以找到其名称。
下面是Type Spec的结构:
- struct ResTable_typeSpec
- {
- struct ResChunk_header header;
- //标识资源的Type ID,Type ID是指资源的类型ID,从1开始。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。
- uint8_t id;
- //保留,始终为0
- uint8_t res0;
- //保留,始终为0
- uint16_t res1;
- //等于本类型的资源项个数,指名称相同的资源项的个数。
- uint32_t entryCount;
- };
下图是其对应的二进制数据:
图6
上图可以看出该chunk的类型是0x0202,这个Type Spec的id是1,entryCount是6E,在这个ResTable_typeSpec结构后面紧跟着entryCount个资源spec数组,entryCount指的是这个类型有多少资源项,在后面我们会讲到aapt解码resources.arsc,输出中每个Type Spec的资源项后面会有一个flags,它的值就是这个数组中对应的值,但是这个flag代表什么我还不清楚。
5.3 Config List
上面讲到每个Type Spec是对一个类型的描述,每个类型会有多个维度,那就是接下来的Config List了,这个Config List是由多个ResTable_type结构来描述的,每个ResTable_type描述的是一个维度,下面是这个结构体的定义:
- struct ResTable_type
- {
- struct ResChunk_header header;
- enum {
- NO_ENTRY = 0xFFFFFFFF
- };
- //标识资源的Type ID
- uint8_t id;
- //保留,始终为0
- uint8_t res0;
- //保留,始终为0
- uint16_t res1;
- //等于本类型的资源项个数,指名称相同的资源项的个数。
- uint32_t entryCount;
- //等于资源项数据块相对头部的偏移值。
- uint32_t entriesStart;
- //指向一个ResTable_config,用来描述配置信息,地区,语言,分辨率等
- ResTable_config config;
- };
其中的id与ResTable_typeSpec中的id值是一样的。其中的ResTable_config就是这个维度的具体描述了,如下:
- struct ResTable_config
- {
- // Number of bytes in this structure.
- uint32_t size;
- union {
- struct {
- // Mobile country code (from SIM). 0 means "any".
- uint16_t mcc;
- // Mobile network code (from SIM). 0 means "any".
- uint16_t mnc;
- };
- uint32_t imsi;
- };
- union {
- struct {
- //