Android资源编译和打包过程分析

这一篇是我们Android热修复学习深入分析的第一篇。
学习总纲计划可以看上一篇文章总纲
首先我们先来分析资源修复相关知识。资源修复的过程基本可以分析为这么一个过程:
老包(线上出现bug需要修复的那个apk包)打包成apk的时候把它需要的资源都打包进去了,然后新的补丁包加入了增加的资源或者需要替换的资源,apk在运行时读取相关资源的时候进行了增加或者替换相关的操作。所以今天我们先来分析资源编译和打包的整个过程。

简介

我们都知道apk其实是一个压缩包,我将一个平时开发的apk解压得到如下目录:
apk解压后的目录 图1

这里我们可以看到,经过编译和打包以后,apk里有:

  1. 二进制的AndroidManifest.xml
  2. assets资源,原封不动的打包到了apk里
  3. classes.dex,java代码编译为dex文件,这里不详述
  4. kotlin代码
  5. lib包
  6. res文件夹,打开可以看到里面都是些二进制文件
  7. resources.arsc,资源索引表。因为Android设备种类繁多,资源索引表的作用就是知道设备的配置信息的情况以后,快速的根据资源ID去匹配到最合适的那个资源。

这里我们重点分析资源文件,Android是通过aapt(Android Asset Package Tool)把资源文件打包到apk里的,也就是上面的2和6,在打包到apk里之前,会先把除了assets资源,res/raw文件资源以外的资源都编译成二进制格式,之所以要编译成二进制文件,原因无非两点:

  1. 空间占用小
  2. 解析速度快

这之后,除了assets资源以外,会给其他所有的资源都生成一个ID,也就是代码里的R.id.xxxxxxxxxx。根据这些ID,打包工具会生成上面我们看到的resources.arsc资源索引表以及一个R.java。资源索引表负责记录所有资源信息,根据资源ID和设备信息,快速的匹配最合适的那个资源。R.java文件则负责记录各个资源ID常量。

那么资源索引表resources.arsc跟R.java文件打开都是什么样的呢?接下来我们就先来看看这两个文件里最终形态到底是怎么样的,根据最后展示给我们的样子再去反推过程会更加容易理解。

解析resources.arsc文件内容

注意:本篇文章Android 源码都出自Android 9.0,
这里,我新建了一个项目,加了各种资源:
图2
然后打包出了一个apk,拿到他的resources.arsc文件对其进行解析:
图3

先来看网上这张神图:
图4,如侵即删,谢谢
这张图基本已经把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的头部得到如下信息:
图5
整个chunk大小位1417151232byte,headerSize = 12, 所以下图中的高亮部分就是我一开始建立的项目apk资源索引表的header部分。
图6

接下来来看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发现内容如下:
图7

接下来就是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的实例:
图8
得到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,都是用来描述一个资源项的具体信息。这又是什么东西呢?

首先我们先来看我们自建项目的资源情况:
图9
这里我们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文件是如何生成的。过程比较复杂,这里我画了一个流程图,下面以流程图为准一步一步的看:

图10

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步骤拆解如下:

  1. 以package为单位,收集类别字符串,例如“drawable”,“string”等。
  2. 以package为单位,收集资源项名称字符串,就比如图2我们建的那个项目,以strings.xml为例,这里我们就收集了"app_name",“jjq”,"hahahaha"三个字符串。
  3. 所有资源项值字符串,再以图2项目为例,就是"ResourceDemo","好帅"和“哈哈哈哈哈哈”;
  4. 生成package数据块,package的数据结构上面解析的时候已经讲过了,这里其实就是把步骤1、2、3获取到的资源一个一个填进去。
  5. 写入资源索引表头部,也就是ResTable_header。
  6. 写入字符串资源池,因为数据都准备好了,所以这里直接写就好了
  7. 写入package,第4步中已经生成好了

10、编译AndroidManifest.xml

现在我们可以编译AndroidManifest.xml文件,将其编译成二进制文件。

11、生成R.java

这里我们已经知道了所有的资源项以及其id,这里我们就可以把他们都写到R.java文件里了。。。这里需要注意的是,R.java里每一个资源类别对应一个内部类,就像这样:
图11
图片上举例了就是两个anim,attr两个类别对应的内部类。

12、打包到apk里

接下来就是打包到apk里了,这里我们会将assets文件目录,res目录下但不包括res/values目录下的资源文件,resources.arsc资源索引文件打包进apk里。

至此,整个Android 资源编译和打包过程就分析完了。。。。。

有了这个基础,接下来我们就可以研究apk运行的时候是如何读取最适合的,相对应的资源文件的。知道了这个过程以后,我们就可以深入探索Android热修复如何才能做到运行的时候去替换资源文件。

本系列目录:
Android热修复原理简要介绍和学习计划

参考文章:
1、《深入探索Android热修复技术原理》
2、老罗:Android应用程序资源的编译和打包过程分析
3、https://blog.csdn.net/jiangwei0910410003/article/details/50628894
4、http://blog.zhaiyifan.cn/2016/02/13/android-reverse-2/

个人微信公共号已上线,欢迎关注:
在这里插入图片描述

发布了31 篇原创文章 · 获赞 23 · 访问量 3万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览