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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值