Android应用程序apk内xml文件编码解析

来源:互联网 发布:连接不上魔法学园网络 编辑:程序博客网 时间:2024/06/09 23:57

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)文档最后还有一个结束名字空间块,结构基本和结束元素块一样,这里就不多做介绍了。

1 0
原创粉丝点击