前言 Android SDK 在编译 Android 工程时,将会把诸如资源文件和清单文件之类的相关 XML 文件编译为特定的二进制格式,目的是为了压缩其容量以及优化其在运行时的解析效率。
将 XML 文件编译为二进制的 XML 文件是 Android 编译资源时的一个子步骤,Android 在完整的资源编译过程结束后将会生成一个 resources.arsc
文件,它是一个资源文件表,应用在运行时会将它映射在内存中,为了资源的查询和引用。编译 Xml 文件为生成 arsc 文件的一个子步骤,如果 Xml 文件中引用了资源,例如字符串资源,那么 Xml 文件中引用字符串的位置将会包含一个全局字串池的索引,通过索引在 arsc 文件中的全局字符串池中即可查询到引用的具体字符串。
有关 arsc 文件的结构和解析方法可参考:Android arsc 文件解析 。
XML 文件结构 编译后的二进制文件结构如下图:
和 arsc 文件的构成方式类似,二进制 XML 文件的结构也是由若干 Chunk 结构组成,且它们在 Android 源码中的 ResourceTypes.h
头文件中均有对应结构的定义。下面分别说明二进制 Xml 中 4 部分 Chunk 的内容。
XML Chunk Header 描述了 XML 文件的基本信息,它在 ResourceTypes.h
中的结构为 struct ResXMLTree_header
,这里使用 Java 描述为:
1 2 3 4 5 6 7 8 9 10 public class ResXMLTreeHeader implements Struct { public ResChunkHeader header; }
其中 ResChunkHeader
为资源 Chunk 的基础描述头部结构,对应的定义为 struct ResChunk_header
,Java 表示为:
1 2 3 4 5 6 7 8 public class ResChunkHeader implements Struct { public short type; public short headerSize; public int size; }
在 XML 文件中,此时的 type
值为 0x003
等于 ResourceTypes.h
中定义的 XML 类型的 type 值:
1 2 3 4 5 public class ResourceTypes {2public static final short RES_XML_TYPE = 0x0003 ; ... }
headerSize
为 ResXMLTreeHeader
头部结构自身的大小,即 ResChunkHeader
的大小,为 8 字节。
size
为当前 Chunk 大小,此时为 XML 文件的大小,包括头结构的大小。
String Pool Chunk String Pool Chunk 为字符串池结构,它包含了此 XML 文件中出现的所有字符串内容。它的结构和 arsc 文件中的全局字符串结构完全一致,下面是引用上篇解析 arsc 文件中的字符串池的描述:
字符串池包括如下几个部分:
ResStringPool_header 字符串池头部,包含字符串池的信息,大小,数量,数组偏移等。
String Offset Array 字符串在字符串内容中的字节位置数组,32 位 int 类型。
Style Offset Array 字符串样式在字符串样式中的字节位置数组,32 位 int 类型。
String Content 字符串内容块。
Style Content 字符串样式块。
字符串池的头部使用 struct ResStringPool_header
数据结构描述,Java 表示为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ResStringPoolHeader implements Struct { public static final int SORTED_FLAG = 1 ; public static final int UTF8_FLAG = 1 << 8 ; public ResChunkHeader header; public int stringCount; public int styleCount; public int flags; public int stringStart; public int styleStart; }
其中 flags
包含 UTF8_FLAG
表示字符串格式为 utf8, SORTED_FLAG
表示已排序。
字符串的偏移数组使用 struct ResStringPool_ref
数据结构描述,Java 表示为:
1 2 3 4 5 6 7 public class ResStringPoolRef implements Struct { public int index; }
字符串样式则使用 struct ResStringPool_span
数据结构描述,Java 表示为:
1 2 3 4 5 6 7 8 9 10 11 12 13 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
,起占位的作用。
Resource Ids Chunk Resource Ids Chunk 包含了 xml 文件中相关的资源 ID,例如 AndroidManifest.xml 中 versionCode
属性的 id 为 0x0101021b
,那么 Resource Ids 块中就包括这个 id 值。
这里 Resource Ids Chunk 的结构很简单,包含一个 struct ResChunk_header
结构的 header
,然后后面包含一个大小为 header.size - header.headerSize
的 int 型 id 数组。
XML Content Chunk XML Content Chunk 是二进制 XML 的核心块结构,包含了 XML 的主要内容。
ResXMLTree_node 此 Chunk 结构包含多种子 Chunk 结构,它们都有相同的头部结构,使用 struct ResXMLTree_node
结构描述,Java 表示为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class ResXMLTreeNode implements Struct { public ResChunkHeader header; public int lineNumber; public ResStringPoolRef comment; }
其中 header
描述此 Chunk 的基本信息,lineNumber
保存了原始行号,comment
则为注释信息。
Start Namespace Chunk
当一个 XML 文件包含命名空间时,首次解析到的 Chunk 为 Start Namespace Chunk
,然后后面解析到和 XML 命名空间数量一致的 Start Namespace Chunk
,它们使用 struct ResXMLTree_namespaceExt
结构描述,Java 表示为:
1 2 3 4 5 6 public class ResXMLTreeNamespaceExt implements Struct { public ResStringPoolRef prefix; public ResStringPoolRef uri; }
其中包含命名空间的 prefix
和 uri
,例如布局文件中经常出现的:
1 xmlns:http://schemas.android.com/apk/res/android="android"
它的 prefix
为 android
,uri
为 http://schemas.android.com/apk/res/android
Start Element Chunk 当有 XML 元素标签出现时,将会解析到 Start Element Chunk
,它表示一个元素标签的开始,使用 struct ResXMLTree_attrExt
结构描述,Java 表示为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ResXMLTreeAttrExt implements Struct { public ResStringPoolRef ns; public ResStringPoolRef name; public short attributeStart; public short attributeSize; public short attributeCount; public short idIndex; public short classIndex; public short styleIndex; }
例如:
1 2 3 <LinearLayout xmlns:http: //schemas.android.com /apk /res /android ="android" xmlns:http: //schemas.android.com /apk /res-auto ="app" layout_gravity ="0x00000050" id ="@0x7f09002b" background ="@0x0106000d" layout_width ="-1" layout_height ="-2" > ... </LinearLayout >
它的 name
为 LinearLayout
,ns
为 null
,attributeCount
等于 5
。
通过 ResXMLTree_attrExt
结构给出的元素信息,可以解析出它所包含的属性,属性使用 struct ResXMLTree_attribute
结构描述,使用 Java 表示为:
1 2 3 4 5 6 7 8 9 10 public class ResXMLTreeAttribute implements Struct { public ResStringPoolRef ns; public ResStringPoolRef name; public ResStringPoolRef rawValue; public ResValue typeValue; }
CData Chunk 当一个元素标签包含值内容时,将会解析到 CData Chunk 结构。
例如 <string>abc</string>
,其中的 CData
为 abc,CData Chunk 使用 struct ResXMLTree_cdataExt
描述,Java 表示为:
1 2 3 4 5 6 public class ResXMLTreeCdataExt implements Struct { public ResStringPoolRef data; public ResValue typeData; }
End Element Chunk End Element Chunk 表示元素标签的结束,和 Start Element Chunk 对应,它使用 struct ResXMLTree_endElementExt
描述,Java 表示为:
1 2 3 4 5 6 public class ResXMLTreeEndElementExt implements Struct { public ResStringPoolRef ns; public ResStringPoolRef name; }
End Namespace Chunk End Element Chunk 表示命名空间的结束符,和 Start Namespace Chunk 对应,描述结构也是一样的。
上述结构对应的 Chunk Header 的 type 值如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ResourceTypes { public static final short RES_XML_TYPE = 0x0003 ; public static final short RES_STRING_POOL_TYPE = 0x0001 ; public static final short RES_XML_START_NAMESPACE_TYPE = 0x0100 ; public static final short RES_XML_END_NAMESPACE_TYPE = 0x0101 ; public static final short RES_XML_START_ELEMENT_TYPE = 0x0102 ; public static final short RES_XML_END_ELEMENT_TYPE = 0x0103 ; public static final short RES_XML_CDATA_TYPE = 0x0104 ; public static final short RES_XML_RESOURCE_MAP_TYPE = 0x0180 ; }
XML 文件解析 为了便于解析,这里使用了我自己写的工具类,参考这里的简介: ObjectIO 。
解析方法 针对上述二进制 XML 文件结构,采用如下方式进行解析:
定义指针变量标识当前解析的字节位置,每解析完一个 chunk 则向下移动指针 chunk 的大小。
采用循环解析的方式,通过 chunk 的 type
判断将要解析哪种 chunk,解析对应的结构。
这里定义了 AXmlParser
解析器,mIndex
为指针变量,parse(ObjectIO objectIO)
为解析子方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private void parse (ObjectInput objectInput) throws IOException { while (!objectInput.isEof(mIndex)) { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_TYPE: ... break ; case ResourceTypes.RES_STRING_POOL_TYPE: ... break ; case ResourceTypes.RES_XML_RESOURCE_MAP_TYPE: ... break ; case ResourceTypes.RES_XML_START_NAMESPACE_TYPE: ... break ; case ResourceTypes.RES_XML_START_ELEMENT_TYPE: ... break ; case ResourceTypes.RES_XML_CDATA_TYPE: ... break ; case ResourceTypes.RES_XML_END_ELEMENT_TYPE: ... break ; case ResourceTypes.RES_XML_END_NAMESPACE_TYPE: ... break ; } } }
首先解析 XML Chunk Header,直接一步就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_TYPE: parseXMLTreeHeader(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 private void parseXMLTreeHeader (ObjectInput objectInput) throws IOException { ResXMLTreeHeader xmlTreeHeader = objectInput.read(ResXMLTreeHeader.class, mIndex); System.out.println(xmlTreeHeader); mIndex += xmlTreeHeader.header.headerSize; }
String Pool Chunk 接下来是符串池的解析了,解析代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void parse (ObjectIO objectIO) { ... ResChunkHeader header = objectIO.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_STRING_POOL_TYPE: parseStringPool(objectIO); break ; ... } ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 ... 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(); System.out.println("string index array:" ); System.out.println(Arrays.toString(stringPoolChunkParser.getStringIndexArray())); System.out.println(); System.out.println("style index array:" ); System.out.println(Arrays.toString(stringPoolChunkParser.getStyleIndexArray())); stringPool = stringPoolChunkParser.getStringPool(); System.out.println(); System.out.println("string pool:" ); System.out.println(Arrays.toString(stringPool)); System.out.println(); System.out.println("style pool:" ); final List<ResStringPoolSpan>[] stylePool = stringPoolChunkParser.getStylePool(); System.out.println(Arrays.toString(stylePool)); System.out.println(); System.out.println("style detail:" ); for (List<ResStringPoolSpan> spans : stylePool) { System.out.println("---------" ); for (ResStringPoolSpan span : spans) { System.out.println(stringPool[span.name.index]); } } mIndex += stringPoolHeader.header.size; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 public class StringPoolChunkParser { private ResStringPoolRef[] stringIndexArray; private ResStringPoolRef[] styleIndexArray; private String[] stringPool; private List<ResStringPoolSpan>[] stylePool; private ResStringPoolRef[] parseStringIndexArray(ObjectIO objectIO, ResStringPoolHeader header, long index) throws IOException { stringIndexArray = new ResStringPoolRef[header.stringCount]; long start = index; final int resStringPoolRefSize = ObjectIO.sizeOf(ResStringPoolRef.class); for (int i = 0 ; i < header.stringCount; i++) { stringIndexArray[i] = objectIO.read(ResStringPoolRef.class, start); start += resStringPoolRefSize; } return stringIndexArray; } private ResStringPoolRef[] parseStyleIndexArray(ObjectIO objectIO, ResStringPoolHeader header, long index) throws IOException { styleIndexArray = new ResStringPoolRef[header.styleCount]; long start = index; final int resStringPoolRefSize = ObjectIO.sizeOf(ResStringPoolRef.class); for (int i = 0 ; i < header.styleCount; i++) { styleIndexArray[i] = objectIO.read(ResStringPoolRef.class, start); start += resStringPoolRefSize; } return styleIndexArray; } private static int parseStringLength (byte [] b) { return b[1 ] & 0x7F ; } private String[] parseStringPool(ObjectIO objectIO, ResStringPoolHeader header, long stringPoolIndex) throws IOException { String[] stringPool = new String[header.stringCount]; for (int i = 0 ; i < header.stringCount; i++) { final long index = stringPoolIndex + stringIndexArray[i].index; final int parseStringLength = parseStringLength(objectIO.readBytes(index, Short.BYTES)); final int stringLength = header.flags == 0 ? parseStringLength * 2 : parseStringLength; stringPool[i] = Formatter.trim(new String(objectIO.readBytes(index + Short.BYTES, stringLength), 0 , stringLength, StandardCharsets.UTF_8)); } return stringPool; } private List<ResStringPoolSpan>[] parseStylePool(ObjectIO objectIO, ResStringPoolHeader header, long stylePoolIndex) throws IOException { @SuppressWarnings("unchecked") List<ResStringPoolSpan>[] stylePool = new List[header.styleCount]; for (int i = 0 ; i < header.styleCount; i++) { final long index = stylePoolIndex + styleIndexArray[i].index; int end = 0 ; long littleIndex = index; List<ResStringPoolSpan> stringPoolSpans = new ArrayList<>(); while (end != ResStringPoolSpan.END) { ResStringPoolSpan stringPoolSpan = objectIO.read(ResStringPoolSpan.class, littleIndex); stringPoolSpans.add(stringPoolSpan); littleIndex += ObjectIO.sizeOf(ResStringPoolSpan.class); end = objectIO.readInt(littleIndex); } stylePool[i] = stringPoolSpans; } return stylePool; } public void parseStringPoolChunk (ObjectIO objectIO, ResStringPoolHeader header, long stringPoolHeaderIndex) throws IOException { final long stringIndexArrayIndex = stringPoolHeaderIndex + ObjectIO.sizeOf(ResStringPoolHeader.class); stringIndexArray = header.stringCount == 0 ? new ResStringPoolRef[0 ] : parseStringIndexArray(objectIO, header, stringIndexArrayIndex); final long styleIndexArrayIndex = stringIndexArrayIndex + header.stringCount * ObjectIO.sizeOf(ResStringPoolRef.class); styleIndexArray = header.styleCount == 0 ? new ResStringPoolRef[0 ] : parseStyleIndexArray(objectIO, header, styleIndexArrayIndex); if (header.stringCount != 0 ) { final long stringPoolIndex = stringPoolHeaderIndex + header.stringStart; stringPool = parseStringPool(objectIO, header, stringPoolIndex); } else { stringPool = new String[0 ]; } if (header.styleCount != 0 ) { final long stylePoolIndex = stringPoolHeaderIndex + header.styleStart; stylePool = parseStylePool(objectIO, header, stylePoolIndex); } else { stylePool = new List[0 ]; } } public ResStringPoolRef[] getStringIndexArray() { return stringIndexArray; } public ResStringPoolRef[] getStyleIndexArray() { return styleIndexArray; } public String[] getStringPool() { return stringPool; } public List<ResStringPoolSpan>[] getStylePool() { return stylePool; } }
Resource Ids Chunk 解析如下,根据 Chunk 头部,解析出 id 数量,然后逐个解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_RESOURCE_MAP_TYPE: parseResourceIds(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void parseResourceIds (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); final int size = header.size; final int count = (size - header.headerSize) / Integer.BYTES; int index = mIndex + header.headerSize; for (int i = 0 ; i < count; i++) { System.out.println("resId: " + Formatter.toHex(Formatter.fromInt( objectInput.readInt(index), true ))); index += i * Integer.BYTES; } mIndex += header.size; }
XML Content Chunk 解析来是解析 XML 内容,每种 XML Chunk 都包含有相同的头部结构 struct ResXMLTree_node
,它包含一个 XML Chunk 块的基本信息和行号,所以需要首先解析它。
Start Namespace Chunk 直接解析即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_START_NAMESPACE_TYPE: parseStartNamespace(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void parseStartNamespace (ObjectInput objectInput) throws IOException { ResXMLTreeNode node = objectInput.read(ResXMLTreeNode.class, mIndex); int namespaceExtIndex = mIndex + node.header.headerSize; System.out.println("node comment: " + (node.comment.index != -1 ? stringPool[node.comment.index] : "" )); final ResXMLTreeNamespaceExt namespaceExt = objectInput.read(ResXMLTreeNamespaceExt.class, namespaceExtIndex); String namespace = stringPool[namespaceExt.prefix.index]; System.out.println("namepsace name: " + namespace); String namespaceUri = stringPool[namespaceExt.uri.index]; System.out.println("namepsace uri: " + namespaceUri); mIndex += node.header.size; }
Start Element Chunk 直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_START_ELEMENT_TYPE: parseStartElement(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private void parseStartElement (ObjectInput objectInput) throws IOException { ResXMLTreeNode node = objectInput.read(ResXMLTreeNode.class, mIndex); System.out.println("node comment: " + (node.comment.index != -1 ? stringPool[node.comment.index] : "" )); int index = mIndex + node.header.headerSize; ResXMLTreeAttrExt attrExt = objectInput.read(ResXMLTreeAttrExt.class, index); String ns = attrExt.ns.index != -1 ? stringPool[attrExt.ns.index] : null ; System.out.println("element ns: " + ns); final String elementName = stringPool[attrExt.name.index]; System.out.println("element name: " + elementName); index += ObjectInput.sizeOf(ResXMLTreeAttrExt.class); for (int i = 0 ; i < attrExt.attributeCount; i++) { ResXMLTreeAttribute attr = objectInput.read(ResXMLTreeAttribute.class, index); final String namespace = attr.ns.index != -1 ? stringPool[attr.ns.index] : null ; System.out.println("attr ns: " + namespace); final String attrName = stringPool[attr.name.index]; System.out.println("attr name: " + attrName); final String attrText = attr.rawValue.index != -1 ? stringPool[attr.rawValue.index] : null ; System.out.println("attr text: " + attrText); final String attrValue = attr.typeValue.dataStr(); System.out.println("attr value: " + attr.typeValue); index += ObjectInput.sizeOf(ResXMLTreeAttribute.class); } mIndex += node.header.size; }
上面的属性数值可能为任何资源类型,其中 Res_Value
结构会包含资源类型,这里根据它的 dataType
简单解析了对应类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public String dataStr () { switch (dataType) { case TYPE_NULL: return "null" ; case TYPE_REFERENCE: return "@" + Formatter.toHex(Formatter.fromInt(data, true )); case TYPE_ATTRIBUTE: return "@:id/" + Formatter.toHex(Formatter.fromInt(data, true )); case TYPE_STRING: return "stringPool[" + data + ']' ; case TYPE_FLOAT: return String.valueOf(data); case TYPE_DIMENSION: int complex = data & (COMPLEX_UNIT_MASK << COMPLEX_UNIT_SHIFT); switch (complex) { case COMPLEX_UNIT_PX: return data + "px" ; case COMPLEX_UNIT_DIP: return data + "dip" ; case COMPLEX_UNIT_SP: return data + "sp" ; case COMPLEX_UNIT_PT: return data + "pt" ; case COMPLEX_UNIT_IN: return data + "in" ; case COMPLEX_UNIT_MM: return data + "mm" ; default : return data + "(dimension)" ; } case TYPE_FRACTION: return data + "(fraction)" ; case TYPE_DYNAMIC_REFERENCE: return data + "(dynamic_reference)" ; case TYPE_INT_DEC: return String.valueOf(data); case TYPE_INT_HEX: return Formatter.toHex(Formatter.fromInt(data, true )); case TYPE_INT_BOOLEAN: return data == 0 ? "false" : "true" ; case TYPE_INT_COLOR_ARGB8: return data + "(argb8)" ; case TYPE_INT_COLOR_RGB8: return data + "(rgb8)" ; case TYPE_INT_COLOR_ARGB4: return data + "(argb4)" ; case TYPE_INT_COLOR_RGB4: return data + "(rgb4)" ; default : return Formatter.toHex(Formatter.fromInt(data, true )); } }
CData Chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_CDATA_TYPE: parseCData(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void parseCData (ObjectInput objectInput) throws IOException { ResXMLTreeNode node = objectInput.read(ResXMLTreeNode.class, mIndex); System.out.println("node comment: " + (node.comment.index != -1 ? stringPool[node.comment.index] : "" )); int index = mIndex + node.header.headerSize; ResXMLTreeCdataExt cdataExt = objectInput.read(ResXMLTreeCdataExt.class, index); final String cdata = stringPool[cdataExt.data.index]; System.out.println("cdata:" + cdata); mIndex += node.header.size; }
End Element Chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_END_ELEMENT_TYPE: parseEndElement(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void parseEndElement (ObjectInput objectInput) throws IOException { ResXMLTreeNode node = objectInput.read(ResXMLTreeNode.class, mIndex); System.out.println("node comment: " + (node.comment.index != -1 ? stringPool[node.comment.index] : "" )); int index = mIndex + node.header.headerSize; ResXMLTreeEndElementExt endElementExt = objectInput.read(ResXMLTreeEndElementExt.class, index); final String ns = endElementExt.ns.index != -1 ? stringPool[endElementExt.ns.index] : "" ; System.out.println("element end ns: " + ns); final String elementName = stringPool[endElementExt.name.index]; System.out.println("element end name: " + elementName); mIndex += node.header.size; }
End Namespace Chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 private void parse (ObjectInput objectInput) throws IOException { ResChunkHeader header = objectInput.read(ResChunkHeader.class, mIndex); switch (header.type) { case ResourceTypes.RES_XML_END_NAMESPACE_TYPE: parseEndNamespace(objectInput); break ; ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void parseEndNamespace (ObjectInput objectInput) throws IOException { ResXMLTreeNode node = objectInput.read(ResXMLTreeNode.class, mIndex); System.out.println("node comment: " + (node.comment.index != -1 ? stringPool[node.comment.index] : "" )); int index = mIndex + node.header.headerSize; ResXMLTreeNamespaceExt namespaceExt = objectInput.read(ResXMLTreeNamespaceExt.class, index); String namespace = stringPool[namespaceExt.prefix.index]; System.out.println("namepsace end name: " + namespace); String namespaceUri = stringPool[namespaceExt.uri.index]; System.out.println("namepsace end uri: " + namespaceUri); mIndex += node.header.size; }
XML 文档输出 上面的解析只是将数值打印出来,看起来,并不直观,所以现在需求是将二进制的 XML 转化为一个可读且格式标准的 XML 文档。
这里首先定义了一个 AXmlEditor
负责将解析的数值输出为一个标准 XML 文档。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 class AXmlEditor { private static final String ACTION_OPEN = "open" ; private static final String ACTION_DATA = "data" ; private static final String ACTION_CLOSE = "close" ; private final StringBuilder xmlBuilder; private String tab = "" ; private List<String[]> namespaceUris = new ArrayList<>(); private String lastAction; AXmlEditor() { xmlBuilder = new StringBuilder(); addHeader(); } private void addHeader () { xmlBuilder.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>" ).append('\n' ); } void openElement (String elementName) { if (ACTION_OPEN.equals(lastAction)) { xmlBuilder.append(">\n" ); } xmlBuilder.append(tab).append('<' ).append(elementName); if (!namespaceUris.isEmpty()) { for (String[] nu : namespaceUris) { xmlBuilder.append(' ' ) .append("xmlns:" ).append(nu[0 ]).append("=\"" ).append(nu[1 ]).append("\"" ); } namespaceUris.clear(); } tab = tab + " " ; lastAction = ACTION_OPEN; } void addNamespace (String namespace, String uri) { namespaceUris.add(new String[]{namespace, uri}); } void addAttribute (String name, String value) { xmlBuilder.append(' ' ) .append(name).append("=\"" ).append(value).append("\"" ); } void addData (String data) { xmlBuilder.append('>' ).append(data); lastAction = ACTION_DATA; } void closeElement (String elementName) { tab = tab.substring(2 ); StringBuilder t = new StringBuilder(); if (ACTION_OPEN.equals(lastAction)) { t.append(">" ); } else if (ACTION_CLOSE.equals(lastAction)) { t.append(tab); } t.append("</" ).append(elementName).append(">\n" ); if (t.toString().contains("><" )) { t.delete(0 , t.length()).append(" />\n" ); } xmlBuilder.append(t); lastAction = ACTION_CLOSE; } String print () { return xmlBuilder.toString(); } }
通过使用上面的解析器即可打印出如下形式的 XML 文档,除了相关资源需要对应的 arsc 文件才能解析出来,输出的文档和标准文档一致。
实例 AM.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:http: //schemas.android.com /apk /res /android ="android" versionCode ="2017082900" versionName ="1.2.455" package ="com.zuoxia.iconpack" platformBuildVersionCode ="25" platformBuildVersionName ="7.1.1" > <uses-sdk minSdkVersion ="16" targetSdkVersion ="25" /> <uses-permission name ="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission name ="android.permission.INTERNET" /> <uses-permission name ="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission name ="android.permission.ACCESS_WIFI_STATE" /> <uses-permission name ="android.permission.SET_WALLPAPER" /> <uses-permission name ="android.permission.SET_WALLPAPER_HINTS" /> <uses-permission name ="android.permission.WAKE_LOCK" /> <uses-permission name ="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission name ="android.permission.VIBRATE" /> <supports-screens anyDensity ="true" smallScreens ="true" normalScreens ="true" largeScreens ="true" resizeable ="true" xlargeScreens ="true" /> <uses-permission name ="com.google.android.c2dm.permission.RECEIVE" /> <permission name ="com.zuoxia.iconpack.permission.C2D_MESSAGE" protectionLevel ="0x00000002" /> <uses-permission name ="com.zuoxia.iconpack.permission.C2D_MESSAGE" /> <application theme ="@0x7f0b0059" label ="@0x7f0800f2" icon ="@0x7f030000" allowBackup ="true" largeHeap ="true" supportsRtl ="true" > <uses-library name ="com.sec.android.app.multiwindow" required ="false" /> <meta-data name ="com.sec.android.support.multiwindow" value ="true" /> <meta-data name ="com.sec.android.multiwindow.DEFAULT_SIZE_W" value ="632.0dip" /> <meta-data name ="com.sec.android.multiwindow.DEFAULT_SIZE_H" value ="598.0dip" /> <meta-data name ="com.sec.android.multiwindow.MINIMUM_SIZE_W" value ="632.0dip" /> <meta-data name ="com.sec.android.multiwindow.MINIMUM_SIZE_H" value ="598.0dip" /> <meta-data name ="com.lge.support.SPLIT_WINDOW" value ="true" /> <activity theme ="@0x7f0b00d1" label ="@0x7f0800f2" name ="com.zuoxia.iconpack.HomeActivity" noHistory ="true" > <intent-filter > <action name ="android.intent.action.MAIN" /> <category name ="android.intent.category.LAUNCHER" /> <category name ="android.intent.category.MULTIWINDOW_LAUNCHER" /> </intent-filter > <meta-data name ="android.app.shortcuts" resource ="@0x7f070008" /> </activity > <service name ="com.zuoxia.iconpack.FirebaseService" > <intent-filter > <action name ="com.google.firebase.MESSAGING_EVENT" /> </intent-filter > </service > <meta-data name ="com.google.firebase.messaging.default_notification_icon" resource ="@0x7f02014d" /> <meta-data name ="com.google.firebase.messaging.default_notification_color" resource ="@0x7f0e0063" /> <activity label ="@0x7f0800f2" name ="jahirfiquitiva.iconshowcase.activities.ShowcaseActivity" > <intent-filter > <action name ="android.intent.action.VIEW" /> <category name ="android.intent.category.DEFAULT" /> <category name ="android.intent.category.BROWSABLE" /> </intent-filter > <intent-filter > <action name ="android.intent.action.SET_WALLPAPER" /> <category name ="android.intent.category.DEFAULT" /> </intent-filter > <intent-filter > <action name ="android.intent.action.GET_CONTENT" /> <category name ="android.intent.category.DEFAULT" /> <category name ="android.intent.category.OPENABLE" /> <data mimeType ="image/*" /> </intent-filter > ... <intent-filter > <action name ="android.intent.action.MAIN" /> <action name ="com.lge.launcher2.THEME" /> <category name ="android.intent.category.DEFAULT" /> </intent-filter > </activity > <activity theme ="@0x7f0b0059" label ="@0x7f0800c6" name ="jahirfiquitiva.iconshowcase.activities.AltWallpaperViewerActivity" /> <service label ="@0x7f0800f9" icon ="@0x7f020149" name ="jahirfiquitiva.iconshowcase.services.MuzeiArtSourceService" description ="@0x7f0800f8" > <intent-filter > <action name ="com.google.android.apps.muzei.api.MuzeiArtSource" /> </intent-filter > <meta-data name ="color" value ="@0x7f0e01be" /> <meta-data name ="settingsActivity" value ="jahirfiquitiva.iconshowcase.activities.MuzeiSettings" /> </service > <activity theme ="@0x7f0b00d1" label ="@0x7f080080" name ="jahirfiquitiva.iconshowcase.activities.MuzeiSettings" exported ="true" /> <receiver label ="@0x7f080064" icon ="@0x7f020064" name ="jahirfiquitiva.iconshowcase.widgets.IconRestorerWidget" > <intent-filter > <action name ="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter > <meta-data name ="android.appwidget.provider" resource ="@0x7f070005" /> </receiver > <activity theme ="@0x7f0b0108" label ="@0x7f0800f2" name ="jahirfiquitiva.iconshowcase.activities.LauncherIconRestorerActivity" noHistory ="true" > <intent-filter > <action name ="android.intent.action.MAIN" /> </intent-filter > </activity > <receiver label ="@0x7f080035" name ="jahirfiquitiva.iconshowcase.widgets.ClockWidget" > <intent-filter > <action name ="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter > <meta-data name ="android.appwidget.oldName" value ="com.android.deskclock.AnalogAppWidgetProvider" /> <meta-data name ="android.appwidget.provider" resource ="@0x7f070003" /> </receiver > <meta-data name ="jahirfiquitiva.iconshowcase.utilities.GlideConfiguration" value ="GlideModule" /> <service name ="com.google.firebase.messaging.FirebaseMessagingService" exported ="true" > <intent-filter priority ="-500" > <action name ="com.google.firebase.MESSAGING_EVENT" /> </intent-filter > </service > <receiver name ="com.google.firebase.iid.FirebaseInstanceIdReceiver" permission ="com.google.android.c2dm.permission.SEND" exported ="true" > <intent-filter > <action name ="com.google.android.c2dm.intent.RECEIVE" /> <action name ="com.google.android.c2dm.intent.REGISTRATION" /> <category name ="com.zuoxia.iconpack" /> </intent-filter > </receiver > <receiver name ="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" exported ="false" /> <service name ="com.google.firebase.iid.FirebaseInstanceIdService" exported ="true" > <intent-filter priority ="-500" > <action name ="com.google.firebase.INSTANCE_ID_EVENT" /> </intent-filter > </service > <provider name ="com.google.firebase.provider.FirebaseInitProvider" exported ="false" authorities ="com.zuoxia.iconpack.firebaseinitprovider" initOrder ="100" /> <meta-data name ="com.google.android.gms.version" value ="@0x7f0d000e" /> </application > </manifest >
实例 layout.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:http: //schemas.android.com /apk /res /android ="android" xmlns:http: //schemas.android.com /apk /res-auto ="app" layout_gravity ="0x00000050" id ="@0x7f09002b" background ="@0x0106000d" layout_width ="-1" layout_height ="-2" > <LinearLayout orientation ="0" id ="@0x7f09002a" background ="@0x7f08044a" paddingTop ="@0x7f07006f" paddingBottom ="@0x7f07006f" layout_width ="-1" layout_height ="-2" baselineAligned ="false" > <RelativeLayout id ="@0x7f0900a1" background ="@:id/0x7f040199" clipChildren ="false" layout_width ="1dip" layout_height ="-2" layout_weight ="1065353216" > <ImageView id ="@0x7f0900a0" padding ="@0x7f07006f" focusable ="false" clickable ="false" clipChildren ="false" layout_width ="-2" layout_height ="-2" layout_marginTop ="@0x7f07006f" layout_marginBottom ="@0x7f07006f" layout_centerInParent ="true" contentDescription ="@0x7f100093" srcCompat ="@0x7f0804ee" /> </RelativeLayout > <RelativeLayout id ="@0x7f090065" background ="@:id/0x7f040199" clipChildren ="false" layout_width ="1dip" layout_height ="-2" layout_weight ="1065353216" > <ImageView id ="@0x7f090064" padding ="@0x7f07006f" focusable ="false" clickable ="false" layout_width ="-2" layout_height ="-2" layout_marginTop ="@0x7f07006f" layout_marginBottom ="@0x7f07006f" layout_centerInParent ="true" contentDescription ="@0x7f100073" srcCompat ="@0x7f0804e3" /> </RelativeLayout > <RelativeLayout id ="@0x7f090024" background ="@:id/0x7f040199" clipChildren ="false" layout_width ="1dip" layout_height ="-2" layout_weight ="1065353216" > <ImageView id ="@0x7f090023" padding ="@0x7f07006f" focusable ="false" clickable ="false" layout_width ="-2" layout_height ="-2" layout_marginTop ="@0x7f07006f" layout_marginBottom ="@0x7f07006f" layout_centerInParent ="true" contentDescription ="@0x7f100038" srcCompat ="@0x7f0804d4" /> </RelativeLayout > <RelativeLayout id ="@0x7f09007a" background ="@:id/0x7f040199" clipChildren ="false" layout_width ="1dip" layout_height ="-2" layout_weight ="1065353216" > <ImageView id ="@0x7f090079" padding ="@0x7f07006f" focusable ="false" clickable ="false" clipChildren ="false" layout_width ="-2" layout_height ="-2" layout_marginTop ="@0x7f07006f" layout_marginBottom ="@0x7f07006f" layout_centerInParent ="true" contentDescription ="@0x7f100080" /> </RelativeLayout > </LinearLayout > </LinearLayout >
源码 AXmlParser 可直接打印标准格式的文档。
参考