前言 Resource.arsc
在apk中,resource.arsc文件储存那些被编译的资源,以及没有被编译的资源的访问路径。resource文件由结构紧凑的二进制编码,分析时需要特别注意细节。 本篇着重对resource.arsc做拆分,弄清楚每块的含义。
1.结构概览
针对网上流传的关于resource.arsc,我在这里做了一个细分,让结构更加清晰。一个Resource大体由三部分组成,Resource_Header记录文件一些重要信息,主要是大小; Global_String_Pool记录了应用中所有的字符串,包括res文件夹下的字符串值;Packages是文件的主体,里面存储了编译过的资源的索引,提供了应用在不同场景下的不同配置值,其实就是对res文件夹下的适配方案做了一个映射编排,详情下文继续分析。
2.头部
2.1头部概念
|
|
前面我们的结构概图中呈现的元素我们称之为Chunk(块),每个Chunk都有一个头部,且每个头部的信息都不一样,但是每个头部的头几位数据是一致,就是上图“块头部”。 一个块头部有三个数据区域,第一位type表示这个Chunk属于什么类型,具体分类如下
|
|
第二位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 就是一个块头部加上一个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的数据结构中,我们知道,想要获取字符串池中的数据,就要使用到图中所示的偏移数组(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方法,就能正确输出。
实现代码如下
|
|
5.资源详情 Package
5.1 资源详情头部 Package_Header
|
|
对示例文件进行解析,得到如下结果
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
id表示类型id,就是0xpptteeee格式中的tt部分。
这里重点介绍entryCount,entryCount表明当前类型下的资源的数量,比如anim资源的数量。
随后紧跟随的数据集合,就是Res_Name_String_Pool中具体资源的配置编码,这个编码有特殊含义,表明该项资源的取值会受到哪些配置影响(这些配置包括 横竖屏,国际化,sdk版本等)
|
|
在代码配置中,如上边的代码所示,每一项配置是占位配置,不是递增配置,这样就可以用一个数据块表明几个配置项。 比如某个资源的获取受到了屏幕大小(ACONFIGURATION_SCREEN_SIZE)和sdk版本(ACONFIGURATION_VERSION)两者的影响 那么这配置项就等于
$$ 0x0200 + 0x0400 = 0x0600 $$
同时没用最高位0x8000这一项,因为最后最高位被置为1时,表示这个资源已经成为公共资源,能够被外部引用。
5.3.2 资源 TypeConfig
前面我们提到了一个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分别表示资源个数,资源数组的相对于头部的偏移,这块其实跟前面的偏移数组计算方式一样,不再多说。
最后我们再看具体的资源分布。
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包体积的作用。了解之后对于资源加载机制的理解也有很大帮助。 后边准备了参考解析源码,是网上某位大牛提供的,能够加深理解。
参考源码