Android arsc 文件解析
文章目录
- Android arsc 文件解析
-
- apk 文件结构
- 资源编译过程
-
- 1. Parse AndroidManifst.xml
- 2. Add Included Resources
- 3. Collection Resource Files
- 4. Add Resources to Resource Table
- 5. Compile Values Resources
- 6. Assign Resource ID to Bag
- 7. Compile Xml Resources
- 8. Add Resource Symbols
- 9. Write resource.arsc
- 10. Compile AndroidManifest.xml
- 11. Write R.java
- 12. Write APK
- arsc 文件结构
- arsc 文件解析
- 源码
- 参考
apk 文件结构
在使用 Android SDK 编译 Android 工程时,它会将工程的源代码和资源打包为一个 apk 文件,apk 文件实质为一个压缩包,一个未签名的 apk 文件典型结构如下:
apk file:
assets/ - assets 原始资源文件
lib/ - so 库文件
res/ - 资源文件
classes.dex - 编译后的代码
resources.arsc - 资源信息文件
AndroidManifest.xml - 二进制的清单文件
在 Android 项目的编译过程中,Java 代码将会被编译为 classes.dex 文件,JNI 代码被编译为 .so 文件存放在 lib 目录下,assets 目录和 res/raw 目录中文件的将不会发生变化,对于资源文件中 xml 形式的资源将会被编译为优化过的特定的二进制 xml 格式,而类似于图片这种本身为二进制的类型也不会发生变化,AndroidManifest.xml 清单文件被编译为优化过的二进制格式。
资源编译过程
在开发 Android 项目时,需要在布局中引用资源,当资源在工程中被创建时,IDE 将会使用 Android SDK 中的 aapt 工具自动生成 R.java 文件,这时在代码或布局文件中即可使用资源 id 来引用资源。
在 aapt 工具生成 R 文件的同时,还同时生成了一个 resources.arsc 文件,它负责记录资源信息,类似于一张表,当 apk 运行在 Android 设备时,应用将会首先将 resources.arsc 包含的资源信息映射到内存中的数据结构中,当需要使用资源 id 引用具体资源时,只需要在内存中的数据结构中进行查询,即可得到具体的资源文件路径。
一个典型的资源 id 如下:
0x7f020000
它由 3 部分组成:
- 首字节为 Package ID,代表资源所在的资源包 ID,一般 apk 中只包含两种资源包,系统资源包和应用资源包,它们的包 ID 分别为 0x01 和 0x7f。
- 次字节为 Type ID,代表资源类型,即 animator、anim、color、drawable、layout、menu、raw、string 和 xml 等类型,每种类型对应一个 id。
- 末两个字节为 Entry ID,代表资源在其类型中的次序。
aapt 工具编译资源的过程是比较复杂的,其中的步骤非常细致,在它编译时会将资源逐步保存至一个 ResourceTable 类中,它的源码路径是 frameworks\base\tools\aapt\ResourceTable
,下面简述资源编译过程(参考了罗升阳的博客)。
1. Parse AndroidManifst.xml
解析 Android 清单文件中的 package 属性,为了提供生成 R 文件的包名。
2. Add Included Resources
添加被引用的资源包,此时会引用系统资源包,在 android 源码工程的 out/target/common/obj/APPS/framework-res_intermediates/package-export.apk
,可通过资源 id 引用其中的资源。
3. Collection Resource Files
aapt 工具开始收集需要编译的资源,将每种类型的资源以及对应的配置维度保存起来。
Type Name Config Data
anim
xml
color
drawable main.png
... sub.png
ok.png mdpi
... hdpi
xhdpi binary
...
4. Add Resources to Resource Table
将资源加入资源表。
Package Type Name Config Path
com.xx.xx anim
xml
color
drawable main.png
... sub.png
ok.png mdpi
... hdpi res/drawable-hdpi/ok.png
xhdpi res/drawable-xhdpi/ok.png
... ...
5. Compile Values Resources
收集 value 下的资源。
Package Name Config Value
string app_name default TestApp
string sub_title default SubTitleText
... ... ... ...
6. Assign Resource ID to Bag
给特殊的 Bag 资源类型分配 id,例如 style,array 等资源。
Type Name Configt Value
style orientation default -
... layout_vertical default 0
layout_horizontal default 1
... ... ...
7. Compile Xml Resources
这一步是将 xml 类型的资源编译为优化过后的二进制格式,便于压缩大小和解析时提高性能。有 6 个子步骤。
-
Parser Xml File(解析原始 xml 文件中的节点树)
XMLNode -elementName -Xml 元素标签 -chars -Xml 元素的文本内容 -attributes -Xml 元素的属性列表 -children -Xml 的子元素
-
Assign Resource IDs(赋予属性名资源 id)
android:layout_width -> find ResID From ResourceTable -> set ResID android:layout_height -> find ResID From ResourceTable -> set ResID android:gravity -> find ResID From ResourceTable -> set ResID ...
-
Parse Values(解析属性的原始值)
android:orientation = horizontal -> 0 ...
对于
@+
符号表示无此 id,则新建此 id。name Config Value et_name default - et_pwd default - ...
-
Flatten(平铺,即转化为最终的二进制格式)
-
Collect Resource ID Strings(收集有资源 id 的属性的名称字符串)
String orientation layout_width layout_height id ... Resource ID 0x010100c4 0x010100f4 0x010100f5 0x010100d0 ...
-
Collect Strings(收集其他字符串)
String android http://schemas.android.com/apk/res/android LinearLayout ...
-
Write Xml header(写入 xml 头部)
-
Write String Pool(依 id 次序写入字符串池)
orientation layout_width layout_height id android http://schemas.android.com/apk/res/android LinearLayout Button
-
Write Resource IDs(写入资源 id 值二进制的 xml 文件中)
-
Flatten Nodes(平铺,即将二进制的 xml 文件中的资源全部替换为资源索引)
-
8. Add Resource Symbols
添加资源符号,根据资源在其资源类型中的位置,为每个资源分配资源 id。
9. Write resource.arsc
将上述收集的资源写入 resoruce.arsc 文件中。分为 7 个步骤。
-
Collect Type Strings(收集每个 package 的类型字符串)
drawable layout string id ...
-
Collect Key Strings(收集每个 package 的资源项的名称字符串)
main icon app_name et_name ...
-
Collect Value Strings(收集资源原始值字符串)
res/drawable-ldpi/icon.png TestApp SubTitleText
-
Generate Package Trunk(生成 package 数据块)
- Write Package Header(写入 package 资源的原信息数据块)
- Write Type Strings(写入类型字符串资源池)
- Write Key Strings(写入资源项名称字符串资源池)
- Write Type Specification(写入类型规范数据块)
- Write Type Info(写入类型资源项数据块)
-
Write Resource Table Header(写入资源表的头部数据块)
-
Write Value Strings( 写入上面收集的资源项的值字符串)
-
Write Package Trunk(写入上面收集的 Package 数据块)
10. Compile AndroidManifest.xml
将 AndroidManifest.xml 编译为二进制。
11. Write R.java
生成 R 文件。
12. Write APK
写入 apk 文件。
arsc 文件结构
arsc 文件作为资源信息的存储结构,其结构将会遵循上述编译过程的写入顺序。整体结构如下图所示:
(图片来自互联网)
arsc 文件的由若干 chunk 结构组成,所有 chunk 在 android 源码中的 ResourceTypes.h
头文件中均有定义,路径为 frameworks\base\include\utils\ResourceTypes.h
。
对于不同 android 版本的 ResourceTypes.h
头文件,为了保证向下兼容性,所以其定义的 chunk 结构相同,不过高版本相对于低版本可能增加了一些配置的常量,例如适配高分辨率设备的 xxhdpi,xxxhdpi 维度选项。
每个 chunk 都会包含一个基础描述类型的对象,它的原始定义如下:
struct ResChunk_header
{
/* Chunk 类型 */
uint16_t type;
/* Chunk 头部大小 */
uint16_t headerSize;
/* Chunk 大小 */
uint32_t size;
};
其中类型 type 的值定义如下:
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
};
它表示每种 chunk 的类型,类似于标识文件类型的魔数,而 chunk 大小 size
则表示此 chunk 的容量。
下面开始对 arsc 文件的结构进行解析,这里使用 java 语言进行解析,为了方便,对于 ResourceTypes.h
中的类型,在 java 中都应该定义对应的类,例如基础描述结构体 ResChunk_header
使用 java 定义如下:
/**
* 资源表 Chunk 基础描述结构。
*/
public class ResChunkHeader {
/** Chunk 类型 */
public short type;
/** Chunk 头部大小 */
public short headerSize;
/** Chunk 大小 */
public int size;
}
arsc 文件解析
为了便于解析,这里使用了我自己写的工具类,参考这里的简介: ObjectIO。
解析方法
针对上述 arsc 文件结构,采用如下方式进行解析:
- 定义指针变量标识当前解析的字节位置,每解析完一个 chunk 则向下移动指针 chunk 的大小。
- 采用循环解析的方式,通过 chunk 的
type
判断将要解析哪种 chunk,解析对应的结构。
这里定义了 ArscParser 解析器,mIndex
为指针变量,parse(ObjectIO objectIO)
为解析子方法。
public class ArscParser {
private int mIndex;
...
private void parse(ObjectIO objectIO) {
// 是否到达文件底部。
while (!objectIO.isEof(mIndex)) {
// 获取将要解析的 chunk 头部信息。
ResChunkHeader header = objectIO.read(ResChunkHeader.class, mIndex);
// 根据类型解析对应格式。
switch (header.type) {
case ResourceTypes.RES_TABLE_TYPE: ...
break;
case ResourceTypes.RES_STRING_POOL_TYPE: ...
break;
case ResourceTypes.RES_TABLE_PACKAGE_TYPE: ...
break;
case ResourceTypes.RES_TABLE_TYPE_SPEC_TYPE: ...
break;
case ResourceTypes.RES_TABLE_TYPE_TYPE: ...
break;
default:
}
}
}
parse RES_TABLE_TYPE
参考上面的 arsc 结构所示,首先解析的是资源表头部,它描述了整个 arsc 文件的大小,以及包含的资源包数量。
它的 type
值为 RES_TABLE_TYPE
,对应的数据结构为 struct ResTable_header
,java 对应的表示为:
/**
* 资源表头结构,对应 ResourceTypes.h 中定义的 ResTable_header。
*/
public class ResTableHeader implements Struct {
/**
* {@link ResChunkHeader#type} = {@link ResourceTypes#RES_TABLE_TYPE}
* <p>
* {@link ResChunkHeader#headerSize} = sizeOf(ResTableHeader.class) 表示头部大小。
* <p>
* {@link ResChunkHeader#size} = 整个 resources.arsc 文件的大小。
*/
public ResChunkHeader header;
/**
* 被编译的资源包数量。
*/
public int packageCount;
}
那么解析代码即:
// ArscParser.java
private void parse(ObjectIO objectIO) {
...
ResChunkHeader header = objectIO.read(ResChunkHeader.class, mIndex);
switch (header.type) {
case ResourceTypes.RES_TABLE_TYPE:
parseResTableType(objectIO);
break;
...
}
...
}
// ArscParser.java
private void parseResTableType(ObjectIO objectIO) {
final ResTableHeader tableType = objectIO.read(ResTableHeader.class, mIndex);
System.out.println("resource table header:");
System.out.println(tableType);
// 向下移动资源表头部的大小。
mIndex += tableType.header.headerSize;
}
测试广点通的 arsc 文件(resources_gdt1.arsc)打印结果如下:
resource table header:
{header={type=2(RES_TABLE_TYPE), headerSize=12, size=6384}, packageCount=1}
parse RES_STRING_POOL_TYPE
接下来是全局字符串池的解析,它包括如下几个部分:
- ResStringPool_header 字符串池头部,包含字符串池的信息,大小,数量,数组偏移等。
- String Offset Array 字符串在字符串内容中的字节位置数组,32 位 int 类型。
- Style Offset Array 字符串样式在字符串样式中的字节位置数组,32 位 int 类型。
- String Content 字符串内容块。
- Style Content 字符串样式块。
字符串池的头部使用 struct ResStringPool_header
数据结构描述,java 表示为:
/**
* 字符串池头部。
*/
public class ResStringPoolHeader implements Struct {
public static final int SORTED_FLAG = 1;
public static final int UTF8_FLAG = 1 << 8;
/**
* {@link ResChunkHeader#type} = {@link ResourceTypes#RES_STRING_POOL_TYPE}
* <p>
* {@link ResChunkHeader#headerSize} = sizeOf(ResStringPoolHeader.class) 表示头部大小。
* <p>
* {@link ResChunkHeader#size} = 整个字符串 Chunk 的大小,包括 headerSize 的大小。
*/
public ResChunkHeader header;
/** 字符串的数量 */
public int stringCount;
/** 字符串样式的数量 */
public int styleCount;
/** 0, SORTED_FLAG, UTF8_FLAG 它们的组合值 */
public int flags;
/** 字符串内容块相对于其头部的距离 */
public int stringStart;
/** 字符串样式块相对于其头部的距离 */
public int styleStart;
}
其中 flags
包含 UTF8_FLAG
表示字符串格式为 utf8, SORTED_FLAG
表示已排序。
字符串的偏移数组使用 struct ResStringPool_ref
数据结构描述,java 表示为:
/**
* 字符串在字符串内容块中的字节偏移。
*/
public class ResStringPoolRef implements Struct{
/** 字符串在字符串池中的索引 */
public int index;
}
字符串样式则使用 struct ResStringPool_span
数据结构描述,java 表示为:
/**
* 字符串样式块中的字符串样式信息。
*/
public class ResStringPoolSpan implements Struct{
public static final int END = 0xFFFFFFFF;
/** 本样式在字符串内容块中的字节位置 */
public ResStringPoolRef name;
/** 包含样式的字符串的第一个字符索引 */
public int firstChar;
/** 包含样式的字符串的最后一个字符索引 */
public int lastChar;
}
其中 name
表示字符串样式本身字符串的索引,比如 <b>
样式本身的字符串为 b,即为 b 在字符串池中的索引。
firstChar
和 lastChar
则为具有样式的字符串的中字符串首位的索引,例如 he<b>ll</b>o
,则为 2 和 3。
字符串样式块和字符串内容块是一一对应的,就是说第一个字符串的样式对应第一个字符串样式块中的样式,如果对应的字符串中有不具有样式的字符串,则对应的 ResStringPool_span
的 name
为 0xFFFFFFFF
,起占位的作用。
解析过程如下:
- 首先解析
ResStringPool_header
,其中包含字符串和样式池的信息。 - 通过
header
中stringCount
(字符串数量) 和styleContent
(样式数量)解析出字符串和样式偏移数组。 - 通过
header
中的stringStart
找到字符串块的起始字节位置,结合字符串偏移数组解析字符串内容。 - 通过
header
中的styleStart
找到样式块的起始字节位置,结合样式偏移数组解析样式内容。
需要注意的是每个字符串的前两个字节表示这个字符串的长度,末尾则为结束符 0。
下面是解析代码:
// ArscParser.java
private void parse(ObjectIO objectIO) {
...
ResChunkHeader header = objectIO.read(ResChunkHeader.class, mIndex);
switch (header.type) {
case ResourceTypes.RES_STRING_POOL_TYPE:
parseStringPool(objectIO);
break;
...
}
...
}
// ArscParser.java
...
private void parseStringPool(ObjectIO objectIO) throws Exception {
final long stringPoolIndex = mIndex;
ResStringPoolHeader stringPoolHeader = objectIO.read(ResStringPoolHeader.class, stringPoolIndex);
System.out.println("string pool header:");
System.out.println(stringPoolHeader);
StringPoolChunkParser stringPoolChunkParser = new StringPoolChunkParser();
stringPoolChunkParser.parseStringPoolChunk(objectIO, stringPoolHeader, stringPoolIndex);
System.out.println();
Syst