前言 Resource.arsc

在apk中,resource.arsc文件储存那些被编译的资源,以及没有被编译的资源的访问路径。resource文件由结构紧凑的二进制编码,分析时需要特别注意细节。 本篇着重对resource.arsc做拆分,弄清楚每块的含义。

1.结构概览

文件结构

针对网上流传的关于resource.arsc,我在这里做了一个细分,让结构更加清晰。一个Resource大体由三部分组成,Resource_Header记录文件一些重要信息,主要是大小; Global_String_Pool记录了应用中所有的字符串,包括res文件夹下的字符串值;Packages是文件的主体,里面存储了编译过的资源的索引,提供了应用在不同场景下的不同配置值,其实就是对res文件夹下的适配方案做了一个映射编排,详情下文继续分析。

2.头部

2.1头部概念

块头部

1
2
3
4
5
struct ResChunk_header {
	uint16_t type;
	uint16_t headerSize;
	uint32_t size;
};

前面我们的结构概图中呈现的元素我们称之为Chunk(块),每个Chunk都有一个头部,且每个头部的信息都不一样,但是每个头部的头几位数据是一致,就是上图“块头部”。 一个块头部有三个数据区域,第一位type表示这个Chunk属于什么类型,具体分类如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum {
    RES_NULL_TYPE               = 0x0000,
    RES_STRING_POOL_TYPE        = 0x0001,
    RES_TABLE_TYPE              = 0x0002,
    RES_XML_TYPE                = 0x0003,
    // Chunk types in RES_XML_TYPE
    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,
    // This contains a uint32_t array mapping strings in the string
    // pool back to resource identifiers.  It is optional.
    RES_XML_RESOURCE_MAP_TYPE   = 0x0180,
    // 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的大小。

2.2大小端

高位地址存高位数据,低位地址存低位数据,这称之为小端模式;反之称之为大端模式,resource.arsc文件采用的小端模式。

1
2
3
00000000: 0200 0c00 5451 0900 0100 0000 0100 1c00  ....TQ..........
00000010: 5892 0100 3c0b 0000 0000 0000 0001 0000  X...<...........
...

从上述数据我们可以看到,示例文件的块头部表示如下

name value
type 0x0002 (类型为 RES_TABLE_TYPE)
headerSize 12 (RES_TABLE_TYPE 的头部大小为 0x000c => 12字节)
size 610644 (整个文件字节数为 0x00095154 => 610644字节)

3.资源头部 Resource_Header

Resource_Header

1
2
3
4
struct ResTable_header{
    struct ResChunk_header header;
    uint32_t packageCount;
};

这里我们开始正式分析,先看资源头部,从上图我们得知,资源头部Resource_Header 就是一个块头部加上一个packages字段。packages表示 了后续的Packages的数量,这里可以看到为0x0000 0001,也就是1个,通常一个工程都对应一个。最后从示例文件中分析,整个资源头部信息如下

name value
type 类型为 RES_TABLE_TYPE
headerSize 12 字节
size 610644 字节
packages 1

4.字符串资源池 Global_String_Pool

Global_String_Pool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct ResStringPool_header
{
    struct ResChunk_header header;
    uint32_t stringCount;//表示字符串数量
    uint32_t styleCount;//表示style的数量
    enum {
        SORTED_FLAG = 1<<0,
        UTF8_FLAG = 1<<8
    };
    uint32_t flags;//是否使用UTF-8方式读取
    uint32_t stringsStart;//字符数组首地址到头部首地址的距离
    uint32_t stylesStart;//style数组首地址到头部地址的距离
};

从Global_String_Pool的数据结构中,我们知道,想要获取字符串池中的数据,就要使用到图中所示的偏移数组(String Offset Array)。 偏移数组中保存的是图中Strings 相应顺序字符串的内存地址的偏移,这个偏移的参照就是Strings的首地址,首地址可以通过下列公式得到

$$ StringsAddr = HeaderAddr+stringsStart $$

也就是Strings的地址等于头部header地址加上stringsStart字段表示的数值。然后我们计算每个字符串的起始地址,图中SOA是String Offset Array的简写。

$$ Strings[i]Addr = SOA[i] + StringsAddr + 2 $$

公式中Strings[i]Addr表示字符串数组中的某一块数据,其地址是SOA和StringsAddr一起计算的,并且加上2是因为每一块的前两个字节表示 字符串的长度。获得了地址之后直接交给 print方法,就能正确输出。

实现代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
uint32_t size = header.header.size - sizeof(struct ResStringPool_header);
unsigned char* pData = (unsigned char*)malloc(size);
fread((void*)pData, size, 1, pFile);
uint32_t* pOffsets = (uint32_t*)pData;
//pData的地址表示header的尾部地址,所以需要减去header的大小
char* pStringsStart = (char*)(pData + header.stringsStart - sizeof(struct ResStringPool_header));
for(int i = 0 ; i < header.stringCount ; i++) {
    //前面两个字节是长度,要跳过
    char* str = pStringsStart + *(pOffsets + i) + 2;
    if(header.flags & ResStringPool_header::UTF8_FLAG) {
	    printf("%s\n", str);
	} else {
        //如果不是UTF-8的存储方式,需要经过特殊处理才能打印
        printUtf16String((char16_t*)str);
	}
}

5.资源详情 Package

5.1 资源详情头部 Package_Header

Package_Header

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct ResTable_package
{
    struct ResChunk_header header;
    uint32_t id;
    uint16_t name[128];
    uint32_t typeStrings;
    uint32_t lastPublicType;
    uint32_t keyStrings;
    uint32_t lastPublicKey;
    uint32_t typeIdOffset;
};

对示例文件进行解析,得到如下结果

feild value feild value
type 512 typeStrings 288
headSize 288 lastPublicType 0
size 507632 keyStrings 572
id 7f lastPublicKey 0
name com.zgh.main typeIdOffset 0

name字段记录了资源关联的包名。

id值为7f,这个id就是我们在R.java文件中经常见到一个资源id中的高两位

$$ pp-tt-eeee $$

比如一个资源id为 0x7f010000,符合0xpptteeee格式,头部这个id就是代表 pp 的部分。并且默认从7f开始 前面的数字是用于系统资源。

再看typeStrings字段和keyStrings字段,这里首先需要知道,头部之后会存储资源的类型和具体资源名两个数据集合, 这两个字段分别表示集合首地址距离头部的首地址的偏移量。这里我们可以计算一下。

首先看typeStrings 为288,表示类型集合距离头部首地址288字节,其实也可以通过头部大小来明白这个值是准确的,头部大小也为 288,而头部之后就是类型集合。

再看keyStrings 为572,表示资源名集合距离头部首地址572,资源名集合位于类型集合之后,我们看看类型集合的大小

1
2
=====================types=====================
type:1, headSize:28, size:284, stringCount:14, stringStart:84, flags:0,styleCount:0, styleStart:0

可以看到类型集合大小size为284 ,刚好符合。

$$ 284 + 288 = 572 $$

5.2 资源字符信息池

头部之后就是两个字符串池,保存的分别是类型名集合和资源名集合。两者在存储结构上跟前文介绍的全局字符串池一样,所以可以直接根据之前的理解读取

5.2.1 资源类型字符串池 Res_Type_String_Pool

我们可以大致先看看类型集合内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
anim
animator
array
attr
bool
color
dimen
drawable
id
integer
layout
raw
string
style

这个集合内容主要表示资源中使用或者涉及的类型,并且这个顺序是后边获取具体的资源项的顺序,是规定好的。

5.2.2 资源项名称字符串池 Res_Name_String_Pool

再看资源名集合的内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...
abc_fade_in
abc_fade_out
abc_grow_fade_in_from_bottom
abc_popup_enter
abc_popup_exit
abc_shrink_fade_out_from_bottom
abc_slide_in_bottom
abc_slide_in_top
abc_slide_out_bottom
abc_slide_out_top
common_push_bottom_in
common_push_bottom_out
...

输出内容是对应类型名集合中的顺序,依次将相应类型的资源顺序输出,并且这个顺序也是后边遍历具体资源配置的顺序。

5.3 资源配置

最后了解资源具体配置之前,我们要明白这一部分的数据大致存储结构

一个Type_Spec开头,后边会紧跟若干个同类型的TypeConfig,Type_Spec的顺序就是前文Res_Type_String_Pool中的顺序。

5.3.1 资源 Type_Spec

Type_Spec

id表示类型id,就是0xpptteeee格式中的tt部分。

这里重点介绍entryCount,entryCount表明当前类型下的资源的数量,比如anim资源的数量。

随后紧跟随的数据集合,就是Res_Name_String_Pool中具体资源的配置编码,这个编码有特殊含义,表明该项资源的取值会受到哪些配置影响(这些配置包括 横竖屏,国际化,sdk版本等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    ACONFIGURATION_MCC = 0x0001,
    ACONFIGURATION_MNC = 0x0002,
    ACONFIGURATION_LOCALE = 0x0004,
    ACONFIGURATION_TOUCHSCREEN = 0x0008,

    ACONFIGURATION_KEYBOARD = 0x0010,
    ACONFIGURATION_KEYBOARD_HIDDEN = 0x0020,
    ACONFIGURATION_NAVIGATION = 0x0040,
    ACONFIGURATION_ORIENTATION = 0x0080,
    
    ACONFIGURATION_DENSITY = 0x0100,
    ACONFIGURATION_SCREEN_SIZE = 0x0200,
    ACONFIGURATION_VERSION = 0x0400,
    ACONFIGURATION_SCREEN_LAYOUT = 0x0800,
    
    ACONFIGURATION_UI_MODE = 0x1000,
    ACONFIGURATION_SMALLEST_SCREEN_SIZE = 0x2000,
    ACONFIGURATION_LAYOUTDIR = 0x4000,

在代码配置中,如上边的代码所示,每一项配置是占位配置,不是递增配置,这样就可以用一个数据块表明几个配置项。 比如某个资源的获取受到了屏幕大小(ACONFIGURATION_SCREEN_SIZE)和sdk版本(ACONFIGURATION_VERSION)两者的影响 那么这配置项就等于

$$ 0x0200 + 0x0400 = 0x0600 $$

同时没用最高位0x8000这一项,因为最后最高位被置为1时,表示这个资源已经成为公共资源,能够被外部引用。

5.3.2 资源 TypeConfig

TypeConfig_header

前面我们提到了一个Type_Spec后边会跟几个TypeConfig,每个TypeConfig不同的地方就是config不同,在图中我们看到中间区域在源码定义为 ResTable_config,ResTable_config用来表示该块TypeConfig主要受什么配置影响,比如TypeConfig受sdk版本影响,那么后续的 config entry array 存储的就是受sdk版本影响的资源项。

比如res文件夹下有values 和 values-v24这两个文件夹,value下有string资源 origin_name ,值为 “value”,value-v24下也有string资源 origin_name,值为“value-v24”

那么origin_name这个字段就会在 config 分别为 默认情况和 config: v24两个TypeConfig下面都出现。

头部id表示当前TypeConfig属于哪个Type_Spec,id 跟 Type_Spec的id是相等的。

然后我们再看偏移数组,entryCount和entriesStart分别表示资源个数,资源数组的相对于头部的偏移,这块其实跟前面的偏移数组计算方式一样,不再多说。

最后我们再看具体的资源分布。

ResTableEntry

ResTableMapEntry

ResTableMapEntry 继承于 ResTableEntry,我们先看两者的公共部分,index表示是该项资源在Res_Name_String_Pool中的位置。 flags用来区分该部分数据为ResTableEntry还是ResTableMapEntry,data就是资源value值。

ResTableEntr用来表示一条简单的资源项,比如一条string的表示如下

1
2
3
4
5
6
7
8
9
<string name="arsc_name">v16demo</string>

==============================================

entryIndex: 0x28, key :

arsc_name 		

value :(string) v16demo

而ResTableMapEntry用来表示比较多的配置项,比如style中的资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimarynew</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>


==============================================

entryIndex: 0x45, key :
AppTheme
			name:0x7f02004e, valueType:1, value:2130968614
			name:0x7f020055, valueType:1, value:2130968616
			name:0x7f020056, valueType:1, value:2130968615

最后我们再次讨论一下一个资源的id组成,比如我在一个string资源中定义了arsc_name,在R.java文件中我我们找到了

1
        public static final int arsc_name = 2131427368;

转化成十六进制为0x7f0b0028,然后我们在arsc文件中去找是符合,entryIndex 为0x28,加上高位0x7f,加上Type_Spec id为 0x0b

$$ 0xpp-tt-eeee $$ $$ 0x7f-0b-0028 $$

总结

Resource.arsc文件包含了资源编译中的索引关系,结构紧凑,起到了一定的缩小apk包体积的作用。了解之后对于资源加载机制的理解也有很大帮助。 后边准备了参考解析源码,是网上某位大牛提供的,能够加深理解。

参考源码

https://github.com/bluesky466/ResourcesArscDemo