简介
我们都知道 apk 其实是一个压缩包, 我将一个平时开发的 apk 解压得到如下目录:
这里我们可以看到, 经过编译和打包以后, apk 里有:
二进制的 AndroidManifest.xml
assets 资源, 原封不动的打包到了 apk 里
classes.dex,java 代码编译为 dex 文件, 这里不详述
kotlin 代码
lib 包
res 文件夹, 打开可以看到里面都是些二进制文件
resources.arsc, 资源索引表. 因为 Android 设备种类繁多, 资源索引表的作用就是知道设备的配置信息的情况以后, 快速的根据资源 ID 去匹配到最合适的那个资源.
这里我们重点分析资源文件, Android 是通过 aapt(Android Asset Package Tool) 把资源文件打包到 apk 里的, 也就是上面的 2 和 6, 在打包到 apk 里之前, 会先把除了 assets 资源, res/raw 文件资源以外的资源都编译成二进制格式, 之所以要编译成二进制文件, 原因无非两点:
空间占用小
解析速度快
这之后, 除了 assets 资源以外, 会给其他所有的资源都生成一个 ID, 也就是代码里的 R.id.xxxxxxxxxx. 根据这些 ID, 打包工具会生成上面我们看到的 resources.arsc 资源索引表以及一个 R.java. 资源索引表负责记录所有资源信息, 根据资源 ID 和设备信息, 快速的匹配最合适的那个资源. R.java 文件则负责记录各个资源 ID 常量.
那么资源索引表 resources.arsc 跟 R.java 文件打开都是什么样的呢? 接下来我们就先来看看这两个文件里最终形态到底是怎么样的, 根据最后展示给我们的样子再去反推过程会更加容易理解.
解析 resources.arsc 文件内容
注意: 本篇文章 Android 源码都出自 Android 9.0, 这里, 我新建了一个项目, 加了各种资源:
然后打包出了一个 apk, 拿到他的 resources.arsc 文件对其进行解析:
先来看网上这张神图:
这张图基本已经把 resources.arsc 的结构画的很清楚了. 最终 resources.arsc 文件是由一系列的 chunk 组成的, 每一个 chunk 都有一个头部, 用来描述 chunk 的元信息. 从图上可以看到, 其实整个资源索引表也可以看成是一个总的 chunk, 头部描述了头大小, 文件大小等参数. 可以理解成设计模式中的组合模式. 解析完一个 chunk 后, 从这个 chunk+size 的位置开始, 就可以得到下一个 chunk 的起始位置, 这样就可以一次读取玩整个文件的数据内容了.
我们来看 chunk_header 的源码, ResChunk_header 源码位于在线源码地址链接:
- /**
- * Header that appears at the front of every data chunk in a resource.
- */
- 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;
- };
type 对应的是 chunk 的类型 headerSize 对应的是 chunk 头部的大小 size 对应的是 chunk 的大小
接着我们再来看下整个资源索引表的头部信息, 也就是 ResourceTableHeader 源码, 源码地址
- /**
- * Header for a resource table. Its data contains a series of
- * additional chunks:
- * * A ResStringPool_header containing all table values. This string pool
- * contains all of the string values in the entire resource table (not
- * the names of entries or type identifiers however).
- * * One or more ResTable_package chunks.
- *
- * Specific entries within a resource table can be uniquely identified
- * with a single integer as defined by the ResTable_ref structure.
- */
- struct ResTable_header
- {
- struct ResChunk_header header;
- // The number of ResTable_package structures.
- uint32_t packageCount;
- };
header 对应的是整个 table 的 header, packageCount 对应的是被编译的资源包的个数
这里我们运行解析 resources.arsc 代码, 解析 Resource Table 的头部得到如下信息:
整个 chunk 大小位 1417151232byte,headerSize = 12, 所以下图中的高亮部分就是我一开始建立的项目 apk 资源索引表的 header 部分.
接下来来看 Global String Pool 部分, 即为资源项的值字符串资源池. 写入字符串资源池的 chunk 同样也是有一个 header 的, 结构如下, 代码地址位于: 添加链接描述
- 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;
- };
header 即为一个 chunk 的 header, stringCount 即为字符串的个数, styleCount 即为字符串样式的个数, stringsStart 和 stylesStart 分别指的是字符串内容与字符串样式的内容相对于其头部的距离.
解析之前我们的项目 apk 发现内容如下:
接下来就是 package 数据块部分了, 按照上面那张神图, 我们先来看其头部部分:
- /**
- * A collection of resource data types within a package. Followed by
- * one or more ResTable_type and ResTable_typeSpec structures containing the
- * entry values for each resource type.
- */
- struct ResTable_package
- {
- struct ResChunk_header header;
- // If this is a base package, its ID. Package IDs start
- // at 1 (corresponding to the value of the package bits in a
- // resource identifier). 0 means this is not a base package.
- uint32_t id;
- // Actual name of this package, \0-terminated.
- uint16_t name[128];
- // Offset to a ResStringPool_header defining the resource
- // type symbol table. If zero, this package is inheriting from
- // another base package (overriding specific values in it).
- uint32_t typeStrings;
- // Last index into typeStrings that is for public use by others.
- uint32_t lastPublicType;
- // Offset to a ResStringPool_header defining the resource
- // key symbol table. If zero, this package is inheriting from
- // another base package (overriding specific values in it).
- uint32_t keyStrings;
- // Last index into keyStrings that is for public use by others.
- uint32_t lastPublicKey;
- uint32_t typeIdOffset;
- };
header 是这个 chunk 的头部信息 id 也就是资源的 package id, 一般 apk 都有两个 id, 一个是系统资源包, id 为 0x01, 还有一个是用户包, 也就是 0x7F,Android 规定 id 在 0x01-0x7F 之间都是合理的, 所以阿里 Sophix 热修复框架在资源修复上就采用了新增一个 package id 为 0x66 的资源包来达到热修复的效果, 这是后话, 之后我们会详细深入, 这里先提一下. name 也就是包名. typeStrings 就是类型字符串资源池相对头部的偏移位置. lastPublicType 指的是最后一个导出的 Public 类型字符串在类型字符串资源池中的索引, 目前这个值设置为类型字符串资源池的大小. keyStrings 指的是资源项名称字符串相对头部的偏移量. lastPublicKey 指的是最后一个导出的 Public 资源项名称字符串在资源项名称字符串资源池中的索引, 目前这个值设置为资源项名称字符串资源池的大小.
根据上面的内容我们再来看我们的项目 apk 的实例:
得到 type = RES_TABLE_PACKAGE_TYPE, typeHexValue = 0x0200, headerSize = 288, headerHexValue = 0x0120, size = 167104, sizeHexValue = 0x00028cc0, id = 127, idHexValue = 0x0000007f name = com.jjq.resourcesarscdemo typeStrings = 288, typeStringsHexValue = 0x00000120 lastPublicType = 0, lastPublicTypeHexValue = 0x00000000 keyStrings = 536, keyStringsHexValue = 0x00000218 lastPublicKey = 0, lastPublicKeyHexValue = 0x00000000.
从上面那张神图上我们可以看到, package 数据块其实包括了: 1,header 2, 资源类型字符串池, 也就是 type string pool 3, 资源项名称字符串池, 也就是 key string pool 4, 类型规范数据块, 也就是 type specification 5, 资源类型项数据块, 也即是 type info
先来看 2 和 3, 实际项目 apk 解析得到如下:
header : type = RES_TABLE_TYPE_SPEC_TYPE, typeHexValue = 0x0202, headerSize = 16, headerHexValue = 0x0010, size = 1060, sizeHexValue = 0x00000424 , id = 2, idHexValue = 0x02, res0 =0 ,res1 = 0 , entryCount = 261, entryCountHexValue = 0x00000105, idValue = imattrboolcolordimendrawableidintegerla realSize = 110 size = 12 c = 2
我们发现已经把一些基本的名称, 类型都已经打印了出来. 接下来来看 type specification 部分:
- /**
- * A specification of the resources defined by a particular type.
- *
- * There should be one of these chunks for each resource type.
- *
- * This structure is followed by an array of integers providing the set of
- * configuration change flags (ResTable_config::CONFIG_*) that have multiple
- * resources for that configuration. In addition, the high bit is set if that
- * resource has been made public.
- */
- struct ResTable_typeSpec
- {
- struct ResChunk_header header;
- // The type identifier this chunk is holding. Type IDs start
- // at 1 (corresponding to the value of the type bits in a
- // resource identifier). 0 is invalid.
- uint8_t id;
- // Must be 0.
- uint8_t res0;
- // Must be 0.
- uint16_t res1;
- // Number of uint32_t entry configuration masks that follow.
- uint32_t entryCount;
- enum : uint32_t {
- // Additional flag indicating an entry is public.
- SPEC_PUBLIC = 0x40000000u,
- // Additional flag indicating an entry is overlayable at runtime.
- // Added in Android-P.
- SPEC_OVERLAYABLE = 0x80000000u,
- };
- };
header 是这个 chunk 的头部信息 id 就是资源的 type id, 每个 type 都会被赋予一个 id. res0 一直是 0, 保留以便以后使用 res1 一直是 0, 保留以便以后使用 entryCount 指的是本类型也就是名称相同的资源个数
转到我们的项目 apk 里, 解析得到如下: header: type = RES_TABLE_TYPE_TYPE, typeHexValue = 0x0201, headerSize = 76, headerHexValue = 0x004c, size = 9424, sizeHexValue = 0x000024d0 , id = 2, idHexValue = 0x02, res0 = 0,res1 = 0, entryCount = 261, entryCountHexValue = 0x00000105,
我们看到一个 id 为 2,type 为 RES_TABLE_TYPE_TYPE, 资源数量为 261 的 chunk.ResTable_typeSpec 后面紧跟着的是一个大小为 entryCount 的 uint32_t 数组, 每一个数组元素都用来描述一个资源项的配置差异性的.
接下来, 我们再来看资源类型项数据块:
- struct ResTable_type
- {
- struct ResChunk_header header;
- enum {
- NO_ENTRY = 0xFFFFFFFF
- };
- // The type identifier this chunk is holding. Type IDs start
- // at 1 (corresponding to the value of the type bits in a
- // resource identifier). 0 is invalid.
- uint8_t id;
- enum {
- // If set, the entry is sparse, and encodes both the entry ID and offset into each entry,
- // and a binary search is used to find the key. Only available on platforms>= O.
- // Mark any types that use this with a v26 qualifier to prevent runtime issues on older
- // platforms.
- FLAG_SPARSE = 0x01,
- };
- uint8_t flags;
- // Must be 0.
- uint16_t reserved;
- // Number of uint32_t entry indices that follow.
- uint32_t entryCount;
- // Offset from header where ResTable_entry data starts.
- uint32_t entriesStart;
- // Configuration this collection of entries is designed for. This must always be last.
- ResTable_config config;
- };
haeder 指的是这个 chunk 的头部信息 id 指的是标识资源的 type id res0,res1,entryCount 同 type spec entriesStart 指的是资源项数据块相对头部的偏移值. config 指的是一个配置信息, 里面包括了地区, 语言, 分辨率等信息
看我们项目的 apk, 解析得到如下信息: header: type = RES_TABLE_TYPE_TYPE, typeHexValue = 0x0201, headerSize = 76, headerHexValue = 0x004c, size = 9424, sizeHexValue = 0x000024d0 , id = 2, idHexValue = 0x02, res0 = 0,res1 = 0, entryCount = 261, entryCountHexValue = 0x00000105, entriesStart = 1120, entriesStartHexValue = 0x00000460 resConfig = size = 0x00000038, imsi = 0x00000000, locale = 0x00000000, screenType = 0x00000000, input = 0x00000000, screenSize = 0x00000000, version = 0x00000000, screenConfig = 0x00000000, screenSizeDp = 0x00000000, localeScript = 0x00000000, localeVariant = 0x00000000
restable_type 后面跟的是一个大小为 entryCount 的 uint32_t 数组, 每一个数组元素都用来描述一个资源项数据块的偏移位置, 紧跟在这个 uint32_t 数组后面的是一个大小为 entryCount 的 ResTable_entry 数组, 每一个数组元素, 即每一个 ResTable_entry, 都是用来描述一个资源项的具体信息. 这又是什么东西呢?
首先我们先来看我们自建项目的资源情况:
这里我们 drawable 类型的资源有 2 个不同的资源和 2 中不同的配置, 其他的比如 string/colors/integers 这种都是有几个 item 选项就几个资源, 只有 1 种配置. 所以我们其实是有类型为 drawable, 配置为 xhdpi; 类型为 drawable, 配置为 xxhdpi; 类型为 string, 配置为 default; 类型为 id, 配置为 default......n+1 个资源项数据块. 这里我们说的资源项数据, 其实就是刚才说的 ResTable_entry.ResTable_entry 结构如下:
- struct ResTable_entry
- {
- // Number of bytes in this structure.
- uint16_t size;
- enum {
- // If set, this is a complex entry, holding a set of name/value
- // mappings. It is followed by an array of ResTable_map structures.
- FLAG_COMPLEX = 0x0001,
- // If set, this resource has been declared public, so libraries
- // are allowed to reference it.
- FLAG_PUBLIC = 0x0002
- };
- uint16_t flags;
- // Reference into ResTable_package::keyStrings identifying this entry.
- struct ResStringPool_ref key;
- };
sizeof 指的是资源头部大小 flag 我们可以看到, 如果是 bag 资源为 1, 如果不是在 public.xml 里定义的, 也就是非 bag 资源, 则为 2 key 也就是资源项名称在资源项名称字符串资源池的索引.
ok, 这里我们基本上对资源索引表的文件格式有了一定了解, 接下来我们就来看这个资源索引表是如何生成的以其其他的一些文件就比如 R.java.
打包流程详解
接下来我们就来着重看看这个 resources.arsc 跟 R.java 文件是如何生成的. 过程比较复杂, 这里我画了一个流程图, 下面以流程图为准一步一步的看:
1, 解析 AndroidManifest.xml
主要做一些检查, 获取 package ID,minSdkVersion,uses-sdk 等属性.
2, 添加被引用资源包
上面我们也讲到了, 通常在编译一个 apk 的时候至少会牵扯到两个资源包, 一个是被引用的系统资源包, 里面包含了很多系统级的, 就比如一个 LinearLayout, 有 layout_width,layout_height,layout_oritation 等属性. 这里有一点要注意, 这里有一个处理重叠包的过程, 其实也就是上面我们讲到的 entryCount(本类型也就是名称相同的资源个数), 如果名称相同, 则使用重叠包.
3, 收集资源文件
这里 aapt 会创建一个 AaptAssets 对象, 将当前需要编译的资源文件根据类别保存下来. 注意, 这里的资源文件指的是除了 values 资源外的资源, 因为 values 资源是在编译的时候进行收集的.
4, 把收集到的资源文件保存到 ResourceTable 对象
这里我们就要新建一个 ResourceTable 对象了, 没错, 就是最上面那张神图, 也就是上面我们叽里呱啦讲了一大堆格式的部分. 第 3 部中, 我们只是把资源文件保存到了 AaptAssets 对象中而已, 这里我们要保存到 ResourceTable 对象中, 在 aapt 源码里对应的是 makeFileResources 函数:
- static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
- ResourceTable* table,
- const sp<ResourceTypeSet>& set,
- const char* resType);
另外注意这一步资源保存指的是除了 values 资源以外的资源, values 资源比较特别, 需要进行编译以后才会保存.
5, 编译 values 资源
values 下的资源都是诸如 strings/colors/ids 这种轻量级的资源, 这些资源都是在编译的时候进行收集的.
6, 给 bag 资源分配 id
bag 资源是什么, bag 资源就是这类资源在赋值的时候, 不能随便赋值, 只能从事先定义好的值中选取一个赋值. 很像枚举, 比如 layout_oritation 这种, 如 attr 资源. 这一步我们会给 bag 资源分配资源 id. 可以理解成给枚举的两个值分配资源 id, 当然这个不是枚举.
7, 编译 xml 文件
ok, 前面的步骤主要是为了给我们编译 xml 文件做准备, 现在开始, 我们就可以编译 xml 文件了. 这里, 程序会对 layouts,anims,animators 等文件逐一调用 ResourceTable.cpp 的如下方法进行编译:
- status_t compileXmlFile(const sp<AaptAssets>& assets,
- const sp<AaptFile>& target,
- ResourceTable* table,
- int options);
内部流程可以分为: 1, 解析 xml 文件: 这一步主要是为了将 xml 文件转化为一系列树形结构 XmlNode 来表示.
2, 赋予属性名称 id: 给每一个资源的属性名称赋予 id. 就比如一个最基本的 button, 他有 layout_width 和 layout_height 两个属性, 这两个属性都属于 bag 资源, 在上一步中我们已经把他们编译了, 这一步就是把编译后的 id 赋值给这个 button. 每一个 xml 都是从根节点开始赋予属性名称 id, 直到该文件下所有节点都有属性 id 了为止.
3, 解析属性值 这一步是第二部的深化, 第二部我们对 layout_width 和 layout_height 这两个属性名称赋予了 id, 这一步我们将对其值进行解析. 仍然是这个 button, 我们将对 match_parent 或者 wrap_content 进行解析.
4, 扁平化为二进制文件 将 xml 改为二进制格式. 步骤分为以下几步: (1), 首先 aapt 会将那些有资源 id 的属性名称收集起来并将他们放在一个数组里. (2), 收集 xml 文件中其他的所有的字符串. (3), 写入文件头, 也就是一个 chunk 的 chunk_header 文件. (4), 将第一步第二步获取到的内容写入 Global String pool 里, 也就是上面解析 resources.arsc 里的字符串资源池中. 具体结构上面解析的时候已经详述. (5), 把所有的资源 id 都收集起来, 生成 package 的时候要用, 也就是上面解析 package 的时候讲到的资源项名称字符串池, 也就是 key string pool. (6), 压平 xml 文件, 也就是把里面的元素都替换掉, 完全变成二进制文件.
8, 给资源生成资源 ID
这里就是给资源生成资源 id,id 是一个 32 位数字, 用十六进制来表示就是 0XPPTTEEEE. PP 为 package id, 也就是上面我们提到的 ResTable_package 数据结构中的 id; TT 位 type id, 也就是我们上面提到的 ResTable_typeSpec 数据结构中的 id; EEEE 为 entry id, 每个 entry 表示一个资源项, 按照先后顺序自动排列, 这里需要注意, 是根据顺序自动排列, 因为这个 entry id 牵扯到热修复更新资源下面的内容, 所以这里需要特别注意, 之后会提到, 这里就不展开了.
9, 根据资源 ID 生成资源索引表
这里我们将生成 resources.arsc 步骤拆解如下:
以 package 为单位, 收集类别字符串, 例如 "drawable","string" 等.
以 package 为单位, 收集资源项名称字符串, 就比如图 2 我们建的那个项目, 以 strings.xml 为例, 这里我们就收集了 "app_name","jjq","hahahaha" 三个字符串.
所有资源项值字符串, 再以图 2 项目为例, 就是 "ResourceDemo","好帅" 和 "哈哈哈哈哈哈";
生成 package 数据块, package 的数据结构上面解析的时候已经讲过了, 这里其实就是把步骤 1,2,3 获取到的资源一个一个填进去.
写入资源索引表头部, 也就是 ResTable_header.
写入字符串资源池, 因为数据都准备好了, 所以这里直接写就好了
写入 package, 第 4 步中已经生成好了
10, 编译 AndroidManifest.xml
现在我们可以编译 AndroidManifest.xml 文件, 将其编译成二进制文件.
11, 生成 R.java
这里我们已经知道了所有的资源项以及其 id, 这里我们就可以把他们都写到 R.java 文件里了... 这里需要注意的是, R.java 里每一个资源类别对应一个内部类, 就像这样:
图片上举例了就是两个 anim,attr 两个类别对应的内部类.
12, 打包到 apk 里
接下来就是打包到 apk 里了, 这里我们会将 assets 文件目录, res 目录下但不包括 res/values 目录下的资源文件, resources.arsc 资源索引文件打包进 apk 里.
至此, 整个 Android 资源编译和打包过程就分析完了.....
有了这个基础, 接下来我们就可以研究 apk 运行的时候是如何读取最适合的, 相对应的资源文件的. 知道了这个过程以后, 我们就可以深入探索 Android 热修复如何才能做到运行的时候去替换资源文件.
写在最后
Android 高级脑图
Android 高级视频
全套高级视频尚在整理完善, 免费分享, 欢迎关注谢谢
来源: http://www.jianshu.com/p/ba8a60080d1c