Android的应用程序apk文件内包含了许多xml文件。大家知道,每一个Android应用程序中都有一个AndroidManifest.xml文件。再比如,Android的Layout也是可以通过xml描述的。如果直接从apk文件中解压出这些xml文件,然后用文本文件打开,会发现根本不可读,是一堆乱码。
这是因为在apk文件中的xml文件是经过编码的,要想得到其原始的可读版本,必须要对其进行解码。
下面,我们来大致介绍一下Android用到的这种xml文件编码格式。
Google对xml文件主要采用的是流式编码方式,从编码中任意截取一块出来是没用的,因为你无法知道它对应的上下文关系,这个节点的父节点是谁,有哪些子节点,都无法获知。Google在对xml编码前,会预先处理一下,提取出一些信息,比如说对所有xml文件中出现的字符串都会分割提取出来。对所有不同类型的信息,都会分块(Chunk)存放。每一个块都有一个头,其格式定义如下:
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;
};
可以看出来,首先的两个字节表示该块的类型,接着两个字节表示该块头的长度,最后4个字节表示该块的整体大小。而块类型有如下定义:
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
};
下面挑几个类型介绍一下:
1)RES_XML_FIRST_CHUNK_TYPE和RES_XML_LAST_CHUNK_TYPE分别表明XML元素块类型的最小值和最大值,只是用来检测错误用的,没有实际用途。
2)RES_STRING_POOL_TYPE表示该块保存的是所有xml文件中使用到的字符串。前面提到过了,在对xml编码之前会提取在该xml文件中出现的所有字符串,然后全部保存到这个块中。由于在xml文件中很多字符串是重复出现的,所以这样做一定程度上可以起到压缩的作用。字符串块的头大小固定为28个字节。其块头具体定义如下:
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;
};
块的最开始是一个块头,包含类型、块头长度和块整体长度的信息。然后4个字节指出该字符串块中包含多少个字符串。接下来四个字节表明有多少个样式。再下来的4个字节是一个Flag,如果最低位为1表明块中的字符串都是排序后存放的,如果第8位为1表明字符串都是用UTF-8编码的。再下来的4个字节,表明具体字符串池存放的位置相对于该字符串块头位置的偏移。最后4个字节,表明样式存放的位置相对于该字符串块头位置的偏移。
紧接着这个块头的是一个偏移值数组,每个偏移4字节,表明要查找的字符串相对于字符串池的偏移。该数组包含的偏移个数由前面字符块头中的stringCount决定。
再下面又是一组偏移值,只不过指向的是样式。如果头中的styleCount值为0,则没有这串信息。
再下面就是字符池的位置了,每个字符串依次存放,对于使用UTF-16编码的xml文件来说,每个字符占用两个字节。字符串的开头有两个字节表明字符串的长度,接着就是字符串本身,最后用0x0000结尾,表明该字符串结束。
再下面就是样式池位置,如果样式值为0,则没有这块。
3)RES_XML_TYPE表示该块存放了一个完整的xml文件。事实上所有编码过后的xml文件,前两个字节一定是0x0003。这种块的头长度固定为8个字节。
4)RES_XML_START_NAMESPACE_TYPE表明开始一个名字空间,在这个块之后出现的所有xml元素块都属于这个名字空间。定义如下:
<pre name="code" class="cpp">/**
* Basic XML tree node. A single item in the XML document. Extended info
* about the node can be found after header.headerSize.
*/
struct ResXMLTree_node
{
struct ResChunk_header header;
// Line number in original source file at which this element appeared.
uint32_t lineNumber;
// Optional XML comment that was associated with this element; -1 if none.
struct ResStringPool_ref comment;
};
......
/**
* Extended XML tree node for namespace start/end nodes.
* Appears header.headerSize bytes after a ResXMLTree_node.
*/
struct ResXMLTree_namespaceExt
{
// The prefix of the namespace.
struct ResStringPool_ref prefix;
// The URI of the namespace.
struct ResStringPool_ref uri;
};
首先还是一个普通的块头,接着的4个字节表示的是这个元素在原始xml文件中出现在哪一行,在下面的四个字节用来指示对应该元素的注释的位置,如果没有注释,则为0xFFFFFFFF。
接下来的4个字节指明该名字空间的前缀,最后的4个字节指明该名字空间的URI。
5)RES_XML_END_NAMESPACE_TYPE表明结束一个名字空间,在这个块之后出现的所有xml元素块都将不属于这个名字空间。其具体格式和RES_XML_START_NAMESPACE_TYPE一样。一般一个xml文档都会以这个块来结尾。6)RES_XML_RESOURCE_MAP_TYPE表明该块存放的是一组资源ID,其定义如下:
enum {
LABEL_ATTR = 0x01010001,
ICON_ATTR = 0x01010002,
NAME_ATTR = 0x01010003,
PERMISSION_ATTR = 0x01010006,
RESOURCE_ATTR = 0x01010025,
DEBUGGABLE_ATTR = 0x0101000f,
VERSION_CODE_ATTR = 0x0101021b,
VERSION_NAME_ATTR = 0x0101021c,
SCREEN_ORIENTATION_ATTR = 0x0101001e,
MIN_SDK_VERSION_ATTR = 0x0101020c,
MAX_SDK_VERSION_ATTR = 0x01010271,
REQ_TOUCH_SCREEN_ATTR = 0x01010227,
REQ_KEYBOARD_TYPE_ATTR = 0x01010228,
REQ_HARD_KEYBOARD_ATTR = 0x01010229,
REQ_NAVIGATION_ATTR = 0x0101022a,
REQ_FIVE_WAY_NAV_ATTR = 0x01010232,
TARGET_SDK_VERSION_ATTR = 0x01010270,
TEST_ONLY_ATTR = 0x01010272,
ANY_DENSITY_ATTR = 0x0101026c,
GL_ES_VERSION_ATTR = 0x01010281,
SMALL_SCREEN_ATTR = 0x01010284,
NORMAL_SCREEN_ATTR = 0x01010285,
LARGE_SCREEN_ATTR = 0x01010286,
XLARGE_SCREEN_ATTR = 0x010102bf,
REQUIRED_ATTR = 0x0101028e,
SCREEN_SIZE_ATTR = 0x010102ca,
SCREEN_DENSITY_ATTR = 0x010102cb,
REQUIRES_SMALLEST_WIDTH_DP_ATTR = 0x01010364,
COMPATIBLE_WIDTH_LIMIT_DP_ATTR = 0x01010365,
LARGEST_WIDTH_LIMIT_DP_ATTR = 0x01010366,
PUBLIC_KEY_ATTR = 0x010103a6,
CATEGORY_ATTR = 0x010103e8,
};
由于这个块是可选的,可有可无,就不多做介绍了。
7)RES_XML_START_ELEMENT_TYPE表明一个xml元素的开始,这是最复杂的一个块
/**
* Extended XML tree node for start tags -- includes attribute
* information.
* Appears header.headerSize bytes after a ResXMLTree_node.
*/
struct ResXMLTree_attrExt
{
// String of the full namespace of this element.
struct ResStringPool_ref ns;
// String name of this node if it is an ELEMENT; the raw
// character data if this is a CDATA node.
struct ResStringPool_ref name;
// Byte offset from the start of this structure where the attributes start.
uint16_t attributeStart;
// Size of the ResXMLTree_attribute structures that follow.
uint16_t attributeSize;
// Number of attributes associated with an ELEMENT. These are
// available as an array of ResXMLTree_attribute structures
// immediately following this node.
uint16_t attributeCount;
// Index (1-based) of the "id" attribute. 0 if none.
uint16_t idIndex;
// Index (1-based) of the "class" attribute. 0 if none.
uint16_t classIndex;
// Index (1-based) of the "style" attribute. 0 if none.
uint16_t styleIndex;
};
struct ResXMLTree_attribute
{
// Namespace of this attribute.
struct ResStringPool_ref ns;
// Name of this attribute.
struct ResStringPool_ref name;
// The original raw string value of this attribute.
struct ResStringPool_ref rawValue;
// Processesd typed value of this attribute.
struct Res_value typedValue;
};
很好理解,先是命名空间的名称;然后是元素的名称;然后是属性结构开始位置的偏移,注意是两个字节;接着是表示紧随这个结构后的属性结构的大小,以字节为单位,也是两个字节;再后面表示该xml元素包含几个属性,也是两个字节;再后面的3个两个字节,风别用来表示该元素中id属性、class属性和style属性的索引,不过是以1为开始的,0表示没有。
接着这个结构体后面是0到多个属性结构体,到底有几个由前面结构中的attributeCount来决定。在结构体中,先是属性所属名字空间的名称,然后是属性的名称,然后是属性值的原始字符串。例如,如果有一个属性是android:versionName="1.0",则名字空间是“android”,属性名称是“versionName”,属性值原始字符串是“1.0”。接下来是一个新的结构体,表示处理过的数据。最开始的两个字节表示该结构体的大小,接着的1个字节一定全是0,接着的一个字节表示处理过后数据的类型,最后的4个字节存储的是实际处理过后的数据,至于如何解释,要看前面类型指定的是什么。
struct Res_value
{
// Number of bytes in this structure.
uint16_t size;
// Always set to 0.
uint8_t res0;
// Type of the data value.
enum {
// Contains no data.
TYPE_NULL = 0x00,
// The 'data' holds a ResTable_ref, a reference to another resource
// table entry.
TYPE_REFERENCE = 0x01,
// The 'data' holds an attribute resource identifier.
TYPE_ATTRIBUTE = 0x02,
// The 'data' holds an index into the containing resource table's
// global value string pool.
TYPE_STRING = 0x03,
// The 'data' holds a single-precision floating point number.
TYPE_FLOAT = 0x04,
// The 'data' holds a complex number encoding a dimension value,
// such as "100in".
TYPE_DIMENSION = 0x05,
// The 'data' holds a complex number encoding a fraction of a
// container.
TYPE_FRACTION = 0x06,
// Beginning of integer flavors...
TYPE_FIRST_INT = 0x10,
// The 'data' is a raw integer value of the form n..n.
TYPE_INT_DEC = 0x10,
// The 'data' is a raw integer value of the form 0xn..n.
TYPE_INT_HEX = 0x11,
// The 'data' is either 0 or 1, for input "false" or "true" respectively.
TYPE_INT_BOOLEAN = 0x12,
// Beginning of color integer flavors...
TYPE_FIRST_COLOR_INT = 0x1c,
// The 'data' is a raw integer value of the form #aarrggbb.
TYPE_INT_COLOR_ARGB8 = 0x1c,
// The 'data' is a raw integer value of the form #rrggbb.
TYPE_INT_COLOR_RGB8 = 0x1d,
// The 'data' is a raw integer value of the form #argb.
TYPE_INT_COLOR_ARGB4 = 0x1e,
// The 'data' is a raw integer value of the form #rgb.
TYPE_INT_COLOR_RGB4 = 0x1f,
// ...end of integer flavors.
TYPE_LAST_COLOR_INT = 0x1f,
// ...end of integer flavors.
TYPE_LAST_INT = 0x1f
};
uint8_t dataType;
......
// The data for this item, as interpreted according to dataType.
uint32_t data;
......
};
8)RES_XML_END_ELEMENT_TYPE表示一个xml元素的结束,其格式如下:
/**
* Extended XML tree node for element start/end nodes.
* Appears header.headerSize bytes after a ResXMLTree_node.
*/
struct ResXMLTree_endElementExt
{
// String of the full namespace of this element.
struct ResStringPool_ref ns;
// String name of this node if it is an ELEMENT; the raw
// character data if this is a CDATA node.
struct ResStringPool_ref name;
};
很简单,首先是元素的名字空间,然后是元素的名字。
那么,这么多块是如何组织的呢?请看下图:
好了,为了方便理解,我们还是结合一个例子来说明吧。笔者写了一个简单的AndroidManifest.xml,其内容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
在将其打包到apk文件中后,得到的二进制AndroidManifest.xml文件内容如下,为了方便解释,一段一段显示:
1)最开始的两个字节是0x0003,表示这个块包含了一个完整的xml文件。接着两个字节是0x0008,表示该块头的长度有8个字节,除去这4个字节,整个头还剩下4个字节。接下来的4个字节0x00000684,表示整个块的大小,也就是该xml文件的大小。
2)接下来的两个字节是0x0001,表示这个块是一个字符串块,其头大小为0x001C个字节,也就是28个字节。接下来4个字节是0x000360,表示该字符串块的长度为864个字节。下面4个字节是0x0000001A,表示共有26个字符串。下面4个字节值是0,表示没有样式。在后面的4个字节是标志位,全0,表明字符串使用UTF-16编码,并且没有排过序。再后面4个字节是0x00000084,表示字符串存放位置的偏移,这个偏移是相对字符串块开头的,如果相对文件开头的话,还要加上8个字节,即0x00000084+8=0x0000008C,刚好可以看到字符串。由于没有样式,所以接下来的4个字节全0。紧接着的28个4字节都是偏移,注意这个偏移是相对于字符串实际存放位置的偏移。举个例子吧,如果想查看字符串块中存放的第二个字符串,则先读出偏移值,本例中是0x0000001A,加上字符串块开始位置0x0000008C,得到地址是0x000000A6,刚好存放的字符串“versionName”。由于没有样式,所以后面接的就是实际的字符串。还是来看刚才举例的字符串,在0x000000A6开始的两个字节是0x0B,表示该字符串由11个字符组成,然后每两个字节表示一个字符,最后有0x0000结尾,表示该字符串终结。
3)接着字符串块的就是所谓的资源ID块,我们接着看:
0x0180表明这是一个资源ID块,该块头长度为8个字节,该块总体长度为0x00000030,即48个字节。紧接着的40个字节,就包含了10个资源ID。第一个是0x0101021B,查前面的定义,可以发现这是android:versionCode属性。后面的都一样我就不解释了。
4)再下面就开始真正表示xml文件的结构了。首先要开始一个名字空间:
0x0100表示开始一个名字空间,其块头长16个字节,块总长24个字节,出现在原始xml文件的第二行,没有注释。名字空间的前缀是字符串池中的第10个字符串,而URI是第11个字符串。我们接着看看第10个字符串是什么,首先找到偏移是0x000000DC,加上开始位置0x0000008C,得到地址是0x00000168,对照着看看这个位置有什么,刚好是字符串“android”的开头。接着的第11个字符串就是“http://schemas.android.com/apk/res/android”。
5)再下面就是xml中的第一个元素了:
这个元素还真不短,我们来逐条分析。0x0102表明这个块表示的是一个元素的开始,块头长16个字节,块的总长度有96个字节,块出现在文档的第二行,没有注释。
下面的就是扩展结构了,0xFFFFFFFF表明该元素没有属于别的名字空间,0x0000000E表示该元素的名字是字符串池中的第14个字符串,即“manifest”。0x0014表示属性的开头距离该扩展结构的开头偏移20个字节,接下来的0x0014表示每个表示属性的结构长20个字节,0x0003表示一共有3个属性,并且紧接着的3个0x00表示没有“id”、“class”和“style”属性。
下面的部分都是用来表示对应于该元素的属性的了。
先来分析第一个属性,最先的4个字节表示的是名字空间,0x0000000B说明是字符串池中的第11个字符,也就是前面介绍过的那个名字空间。不过在xml文档中,不是显示完整的名字空间URI,而是显示名字空间前缀,所以这里对应的就是“android:”。下面的4个字节表示属性的名字,0x00000000表示字符串池中的第0个字符串,也就是“versionCode”。下面的4字节0x00000000表示没有记录初始的属性值字符串。既然没有记原始的,那肯定属性值就在处理过的数据里了。0x0008表示接下来的结构体长8个字节,下面的1个字节一定是0,再下面的一个字节0x10,表示这个属性值是整形,下面的4个字节0x00000001就表示这个整形值是1。所以,可以看出,这个属性是android:versionCode=1。
再来看第二个属性,名字空间一样,就不解释了。下面的0x00000001表示该属性的名字是字符串池中的第1个字符串,也就是“versionName”。下面的0x00000010表示记录了初始的属性值字符串,是字符串池中的第16个字符串,查一下刚好是“1.0”。再下来可以看出处理过后的数据类型是0x03,即字符串类型,而接着的值记得是0x00000010,由于处理过后也当做字符串类型,所以和前面的初始值一样。所以,这个属性是android:versionName="1.0"。
好,接着来看最后一个属性。首先名字空间指定为0xFFFFFFFF(-1),表示没有名字空间前缀。属性名称指定为0x0000000D,表示是字符串池中的第13个字符串,也就是“package”。下面的0x0000000F表示属性值的原始字符串为字符串池中的第15个字符串,即“com.example.test”。而处理过后的属性值仍然为字符串型(0x03)。所以,这个属性为package="com.example.test"。
6)接着,还有一些元素,分析方法和前面一样,我直接跳过,如果有兴趣可以自行分析。我们下面来看一看对应前面开始元素的结束元素是什么样的:
0x0103表明这个块是用来结束一个名字空间的,0x0010表示该块的头有16个字节长,0x00000018表明该块总共有24个字节长,0x0000001B表示这个元素出现在原来xml文件中的第29行,0xFFFFFFFF表示没有注释。再下来的0xFFFFFFFF表示没有自己的名字空间,0x0000000E表示元素名是字符串池Hong的第14个字符串,即“manifest”。
7)文档最后还有一个结束名字空间块,结构基本和结束元素块一样,这里就不多做介绍了。