Android arsc 文件解析

本文详细介绍了Android资源编译过程,从apk文件结构到arsc文件的生成,包括ResourceTable的构建、资源ID的分配,以及arsc文件的结构和解析方法。在编译过程中,aapt工具生成的arsc文件存储了资源信息,并在运行时映射到内存,用于快速查找资源。解析arsc文件涉及RES_TABLE_TYPE、RES_STRING_POOL_TYPE、RES_TABLE_PACKAGE_TYPE、RES_TABLE_TYPE_SPEC_TYPE和RES_TABLE_TYPE_TYPE等chunk类型。
摘要由CSDN通过智能技术生成

Android 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 部分组成:

  1. 首字节为 Package ID,代表资源所在的资源包 ID,一般 apk 中只包含两种资源包,系统资源包和应用资源包,它们的包 ID 分别为 0x01 和 0x7f。
  2. 次字节为 Type ID,代表资源类型,即 animator、anim、color、drawable、layout、menu、raw、string 和 xml 等类型,每种类型对应一个 id。
  3. 末两个字节为 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 个子步骤。

  1. Parser Xml File(解析原始 xml 文件中的节点树)

    XMLNode
    -elementName   -Xml 元素标签
    -chars         -Xml 元素的文本内容
    -attributes    -Xml 元素的属性列表
    -children      -Xml 的子元素
    
  2. 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
     ...
    
  3. Parse Values(解析属性的原始值)

    android:orientation = horizontal -> 0
    ...
    

    对于 @+ 符号表示无此 id,则新建此 id。

    name            Config         Value
    et_name         default        -
    et_pwd          default        -
    ...
    
  4. Flatten(平铺,即转化为最终的二进制格式)

    1. Collect Resource ID Strings(收集有资源 id 的属性的名称字符串)

      String         orientation  layout_width  layout_height  id           ...
      Resource ID    0x010100c4   0x010100f4    0x010100f5     0x010100d0   ...
      
    2. Collect Strings(收集其他字符串)

      String android  http://schemas.android.com/apk/res/android  LinearLayout ...
      
    3. Write Xml header(写入 xml 头部)

    4. Write String Pool(依 id 次序写入字符串池)

      orientation  layout_width  layout_height  id  android  http://schemas.android.com/apk/res/android  LinearLayout Button
      
    5. Write Resource IDs(写入资源 id 值二进制的 xml 文件中)

    6. Flatten Nodes(平铺,即将二进制的 xml 文件中的资源全部替换为资源索引)

8. Add Resource Symbols

添加资源符号,根据资源在其资源类型中的位置,为每个资源分配资源 id。

9. Write resource.arsc

将上述收集的资源写入 resoruce.arsc 文件中。分为 7 个步骤。

  1. Collect Type Strings(收集每个 package 的类型字符串)

     drawable  layout  string  id  ...
    
  2. Collect Key Strings(收集每个 package 的资源项的名称字符串)

    main icon  app_name  et_name ... 
    
  3. Collect Value Strings(收集资源原始值字符串)

    res/drawable-ldpi/icon.png  TestApp  SubTitleText
    
  4. Generate Package Trunk(生成 package 数据块)

    1. Write Package Header(写入 package 资源的原信息数据块)
    2. Write Type Strings(写入类型字符串资源池)
    3. Write Key Strings(写入资源项名称字符串资源池)
    4. Write Type Specification(写入类型规范数据块)
    5. Write Type Info(写入类型资源项数据块)
  5. Write Resource Table Header(写入资源表的头部数据块)

  6. Write Value Strings( 写入上面收集的资源项的值字符串)

  7. 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 文件结构,采用如下方式进行解析:

  1. 定义指针变量标识当前解析的字节位置,每解析完一个 chunk 则向下移动指针 chunk 的大小。
  2. 采用循环解析的方式,通过 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

接下来是全局字符串池的解析,它包括如下几个部分:

  1. ResStringPool_header 字符串池头部,包含字符串池的信息,大小,数量,数组偏移等。
  2. String Offset Array 字符串在字符串内容中的字节位置数组,32 位 int 类型。
  3. Style Offset Array 字符串样式在字符串样式中的字节位置数组,32 位 int 类型。
  4. String Content 字符串内容块。
  5. 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 在字符串池中的索引。

firstCharlastChar 则为具有样式的字符串的中字符串首位的索引,例如 he<b>ll</b>o,则为 2 和 3。

字符串样式块和字符串内容块是一一对应的,就是说第一个字符串的样式对应第一个字符串样式块中的样式,如果对应的字符串中有不具有样式的字符串,则对应的 ResStringPool_spanname0xFFFFFFFF,起占位的作用。

解析过程如下:

  1. 首先解析 ResStringPool_header,其中包含字符串和样式池的信息。
  2. 通过 headerstringCount(字符串数量) 和 styleContent(样式数量)解析出字符串和样式偏移数组。
  3. 通过 header 中的 stringStart 找到字符串块的起始字节位置,结合字符串偏移数组解析字符串内容。
  4. 通过 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值