美文网首页
Android 编译打包的那些疑问

Android 编译打包的那些疑问

作者: 猪_队友 | 来源:发表于2019-05-27 11:32 被阅读0次

    我们平时都是用 AS 进行打包,这就造成了很多盲点,我们就来看看究竟是咋回事,提前声明这篇文章讲的不全,只讲一些疑惑盲点,需要全面学习的,看老罗的吧,详细的令人发指。

    我们从结果入手,看看打包完毕的apk里面是啥模样,把.apk 修改成 .zip 解压缩。按图索骥哈哈。

    image

    一共五个文件,第一个 AndroidManifest.xml 我们很熟悉了。打开看一下。

    image

    我的个乖乖,咋回事。
    然后在打开 res 文件,很多维度的 drawable 文件夹啊。

    image

    随便打开一个吧,看看里面的 xml 文件,nm,咋也都是二进制数据了呢?

    image

    好吧,这些貌似是和 资源编译打包有关系哈,印象里好像是这么回事,赶紧百度,谷歌。看到了如下的神图貌似和我们遇到的类似啊。

    image image

    aapt 这家伙,把我们的资源搞了一下。他想干什么呢?带着疑问我们去学习。

    1、为啥要把文本格式的xml 转换成 二进制格式的

    为啥呢?如果猜测的话,应该不是闲的没事搞得,我们知道安卓面临的问题一直是空间和时间的问题,这么搞,无非就是省空间,提升速度。

    2、怎么做呢?

    把所有 xml 元素的标签,属性,内容等字符串统一放到一个 字符串资源池里面去,然后在用到这个字符串的地方用索引来代替,这是偷懒的行家啊,也是计算机里的局部思想的发扬光大。这样就可以解决空间占用的大小了。
    那么怎么就速度快了呢?因为这里的字符串用的是索引,所以就不必每次都解析字符串了,这还不快吗?重复利用多开心啊。

    ok,这个 xml 二进制的问题也就解决完毕。但是一个问题的结束往往伴随着另一个问题的开始。那个字符串资源池在哪里呢?

    这就引出了我们的上面 五大部分的 Resources.arsc。先容我百度,谷歌下哈。
    https://blog.csdn.net/beyond702/article/details/51744082
    很显然我不是要解析 Resources.arsc,反正我知道了,这个字符串资源池就在 Resources.arsc 中,名字叫 Global String Pool,这样就可以了。

    好了回到 aapt 这个家伙。

    据网上总结他有以下几个重要的工作。

    1、 assert 和 res/raw 目录下的所有资源原封不动打包到 apk 里。
    2、对 res/ 目录下的文件进行编译处理 比如 xml 编译成二进制文件,png 等图片也会进行优化处理。
    3、除了 assert 资源之外的所有资源都会赋予一个资源ID常量,并且声称一个资源索引表 Resources.arsc。
    4、把 AndroidManifest.xml 也进行二进制处理。
    5、把上面四步骤中声称的结果保存到一个*.ap_ 文件,把各个资源 ID 常量定义在 R.java 文件中。

    这么一来解答了不少疑惑,但是 *.ap_ 是个啥玩意呢?下图是网上的,说实话,我反正没看到,我实验了下没有 .ap 文件。咋办呢?继续搜吧,可能是文章有点老了。

    image image

    还是看官方文档吧,我擦,appt2 了啊,好吧,看英文文档使我快乐(😢)。文档上说 appt 已经弃用,开始使用 appt2 了,虽然老项目也在用,但是你懂得。

    https://developer.android.com/studio/command-line

    image

    终于找到了一篇文章,讲述 appt2 编译的。

    https://www.colabug.com/1787983.html

    原来是 appt2 将原来的资源编译打包过程拆分成了两部分,编译和链接,提高了资源的编译性能。当只有一个资源发上改变的时候,只需要重新编译改变的文件就行了。其他不变的进行链接就行了。之前 appt 是将所有的资源进行merge ,merge 完毕重新对所有资源进行编译,产生一个 资源 ap_ 文件。这个也就是一个压缩包。

    image

    具体细节大家有兴趣可以搞一搞。我心里还是有点不明白这些个过程,所有又找了一篇文章来看看。

    https://www.jianshu.com/p/d487f0aa9818

    这个的图真是详细的很啊。


    image

    我们要编译的应用程序的资源结构目录。图文结合一下,不然不知道说的什么。

    image

    第一步:解析AndroidManifest.xml

    获得包名,根据包名创建资源表 ResourceTable 。
    那什么是 ResourceTable 呢?Android资源打包工具在编译应用资源之前,会创建一个资源表,当编译完成后,就可以拿着这个资源表,去生成资源索引文件 resources.arsc。

    第二步:添加被引用资源包

    不光我们自己的应用拥有资源包,android 系统也定义了一套通用资源。所以需要把这个也添加上。最重要的的 资源ID 的命名规则是这样的。一共四位,Package ID,次高字节表示Type ID,最低两字节表示Entry ID。

    Package ID:比如系统的就是0x01 ,我们自己的就是0x7f。

    Type ID:资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。

    Entry ID:是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。

    总结就是 先按包名来分,然后看类型,最后看顺序。

    第三步:收集资源文件

    在编译应用程序之前,aapt 会创建一个 AaptAsserts 对象。用来收集当前需要编译的资源文件。保存到 AaptAsserts 的成员变量 mRes。

    KeyedVector<String8, sp<ResourceTypeSet> >* mRes;
    

    收集资源是按照类型来保存的 分别是 drawable、layout、values。所以对应了三种 ResourceTypeSet。

    举个例子说明下吧(其实我就是抄的)

    1、类型是 drawalbe 的ResourceTypeSet 只有一个AaptGroup,它的名称是 icon.png。这个AaptGroup 里包含了三个文件 res/drawable-ldpi/icon.png、res/drawable-mdpi/icon.png和res/drawable-hdpi/icon.png。
    每一个文件都用一个 AaptFile 来描述,并且都对应一个 AaptGroupEntry。每个 AaptGroupEntry 描述的都是不同的资源配置信息,即他们所描述的屏幕密度是ldpi、mdpi和hdpi。

    2、类型是 layout 的的ResourceTypeSet 有两个AaptGroup,分别是 main.xml 和 sub.xml。都只包含了一个 AaptFile ,分别是res/layout/main.xml和res/layout/sub.xml。同样分别对应一个AaptGroupEntry。这两个AaptGroupEntry描述的资源配置信息都是属于default的。

    3、类型为 values 的ResourceTypeSet 只有一个 AaptGroup,为 string.xml。包含了一个 AaptFile 即 res/values/strings.xml。同样对应一个AaptGroupEntry,这个AaptGroupEntry描述的资源配置信息也是属于default的。

    第四步:将收集到的资源添加到资源表

    上一步只是保存到 AaptAsserts 对象里,我们需要将这些资源同时增加到 ResourceTable 对象中,为啥子呢?因为我们要用 ResourceTable 来生成 resources.arsc。这样看来思路就有点清晰了。

    需要注意的是: 收集的资源不包括 values 类型的资源,它比较特殊,要经过编译才会添加到资源表中。(ps:又增加了一个问题)

    举个例子:

    在这个名称为“shy.luo.activity”的Package中,分别包含有drawable和layout两种类型的资源,每一种类型使用一个Type对象来描述,其中:

    1、类型是 drawable 的Type,包含一个 ConfigList。名称为 icon.png。包含了三个 Entry,分别是res/drawable-ldpi/icon.png、res/drawable-mdpi/icon.png和res/drawable-hdpi/icon.png。每一个 Entry 对应一个 ConfigDescription,用来描述不同的资源配置信息。

    image image
    image

    第五步:编译 values 类资源

    类型为 values 终于开始要编译了,之前的疑问看来要在这里进行解答了。我们通常用 values 来描述一些简单的值,比如 颜色,大小,尺寸等等。这些资源是在编译的过程中收集的。具体怎么收集看下边。

    strings.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="app_name">Activity</string>
        <string name="sub_activity">Sub Activity</string>
        <string name="start_in_process">Start sub-activity in process</string>
        <string name="start_in_new_process">Start sub-activity in new process</string>
        <string name="finish">Finish activity</string>
    </resources>
    

    这个文件经过编译后,资源表中会多了一个名为 string 的 Type。这个 Tpye 还有五个 ConfigList 。这五个 ConfigList 的名称分别为 “app_name”、“sub_activity”、“start_in_process”、“start_in_new_process”和“finish”,每一个ConfigList又分别含有一个Entry。

    image

    第六步、给Bag资源分配ID

    类型 values的资源除了 string 之外,还会有 bag,style,array 等。统一称为 Bag 资源。比如 Android 系统提供的android:orientation属性的取值范围为{“vertical”、“horizontal”},就相当于是定义了vertical和horizontal两个Bag。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <attr name="custom_orientation">
            <enum name="custom_vertical" value="0" />
            <enum name="custom_horizontal" value="1" />
        </attr>
    </resources>
    
    image

    看完了 Bag 的解释。我们看看是如何分配的ID的。上面是三个Entry均为 Bag 资源。其中 custom_vertical(id类型资源)和custom_horizontal( id类型资源)是custom_orientation(attr类型资源)的两个bag。我们可以将custom_vertical和custom_horizontal看成是custom_orientation的两个元数据,用来描述custom_orientation的取值范围。实际上,custom_orientation 还有一个内部元数据,用来描述它的类型。这个内部元数据也是通过一个 bag 来表示的,这个 bag 的名称和值,分别是“^type”和TPYE_ENUM ,用来表示他描述的一个枚举类型的属性。注意:所有“^”开头的bag都是表示一个内部元数据。

    对于 Bag 资源来说,这一步需要给他们的元数据项分配资源ID,也就是给他们的bag分配资源ID,例如上述的 custom_orientation 来说,我们需要给它的 ^type 、custom_horizontal 和 custom_vertical 分配资源ID。其中 ^type 分配到的是 attr 类型的资源ID,而custom_vertical和custom_horizontal分配的是 id 类型的资源ID。

    第七步:编译xml资源文件

    前六步都是为了编译xml资源文件做准备。不容易啊。

    开始编译:

    除了 values 类型的资源文件,其他所有xml资源文件都需要编译。以 main.xml 为例。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" 
        android:gravity="center">
        <Button 
            android:id="@+id/button_start_in_process"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@string/start_in_process" >
        </Button>
        <Button 
            android:id="@+id/button_start_in_new_process"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@string/start_in_new_process" >
        </Button>
    </LinearLayout>
    
    image

    1、解析xml 文件 这个就不多说了,最后得到根节点的 XMLNode。
    2、赋予属性名称资源ID。
    比如根节点 LinearLayout 里面有“android:orientation”、“android:layout_width”、“android:layout_height”和“android:gravity”都需要赋予一个资源ID。这就给是在系统资源包里定义的。所以AAPT会从系统资源包里找到这些名称对应的资源ID,然后才能赋给main.xml 的根节点LinearLayout。

    注意:对于系统资源包来说“android:orientation”、“android:layout_width”、“android:layout_height”等这些属性名称都是它定义的一系列 Bag资源。在被编译的时候就分配好资源ID了。如第六步。

    每一个xml文件都是从根节点开始给属性名称赋予资源ID的,然后在递归给每一个子节点的属性名称赋予资源ID。直到都获得为止。
    3、解析属性值。
    上一步只是对属性的解析,这一步是对属性值的解析。比如 main.xml 文件的根节点 LinearLayout 来说,我们已经给他的属性 android:orientation 赋值了一个资源 ID,这里就是要给他的值 vertical 进行解析。上面说到了,android:orientation 是 Bag 资源,这个 Bag 资源分配有资源 ID,也会有元数据,也就是它的取值。对于 android:orientation 的合法取值就是 horizontal 或者 vertical 。而这两个也是 Bag 资源。他们的值分别被定义为 0 和 1。

    AAPT 是如何找到 main.xml ->LinearLayout-> android:orientation->vertical 等于 1 的呢?假设上一步从系统资源找到资源 ID 0x010100c4,那么 AAPT 会找到它的元数据,也就是 名为 horizontal 和 vertical 的 Bag。接着根据字符串匹配到 vertical 的 Bag,最后就可以将这个 Bag 解析了。

    讲个很基础的一个东西,平时经常用,但是基本不会注意的一个点,对于引用类型的属性值,比如 android:id属性值“@+id/button_start_in_process”,其中 @ 表示后面描述的属性是因引用类型的。+ 表示如果该引用不存在那么就新建一个。id 表示引用的资源类型是 id。button_start_in_process 是个名称。实际上在 id 之前还可以加 包名,@+[package:]id/button_start_in_process 就是这样的,如果不指定那就从当前的包里查找。

    再举个例子,比如 android:text属性值“@string/start_in_process”,在第五步的时候已经编译过了,所以在这里可以直接获取他们的资源 ID。

    注意:一个资源项一旦建立之后,要获得它的 ID 是很容易的,因为它的 package id、tpye id和 entry id都是已知的。

    4、压平xml
    这个词很新鲜啊,第一次听说这么个东西,就是对xml文件的内容进行扁平化处理,实际就是将xml文件格式转换成二进制格式。过程如下图。

    image

    一共分六步,(mmp好复杂)
    step 1、收集有资源 ID 的属性的名称字符串
    这一步除了会收集 资源 ID的属性的名称字符串之外,还会将对应的资源ID收集到一个数组中,这里收集到的属性名称字符串保存在一个字符串资源池中。他们与收集到的资源ID是一一对应的。也就是下图这样子滴。

    image

    step 2、收集其他字符串
    看到第一步我还在纳闷,怎么收集的字符串就只有属性的呢?原来还有其他的也放入字符串资源池里,不过对于字符不会重复收集,毕竟是字典嘛。

    image

    step 3、写入XML文件头
    最终编译出来的XML二进制文件是一系列的chunk组成,每一个chunk都有一个头部,用来描述chunk的元信息,同时整个xml文件又可以看成一个总的chunk。它有一个类型为 ResXMLTree_header的头部。

    struct ResChunk_header
    {
        uint16_t type;
        uint16_t headerSize;
        uint32_t size;
    };
     
    struct ResXMLTree_header
    {
        struct ResChunk_header header;
    };
    
    

    --type:等于RES_XML_TYPE,描述这是一个Xml文件头部。

    --headerSize:等于sizeof(ResXMLTree_header),表示头部的大小。

    --size:等于整个二进制Xml文件的大小,包括头部headerSize的大小。

    step 4、写入字符串资源池
    原来定义在xml文件中的字符串已经在 1、2步收集完毕,因此,我们可以将它们写入最终收集到二进制格式的xml文件中。写入的顺序必须严格按照在字符串资源池中的写入顺序。例如,对于main.xml来说,依次写入的字符串为“orientation”、“layout_width”、“layout_height”、“gravity”、“id”、"text"、"android"、“http://schemas.android.com/apk/res/android”、“LinearLayout”和“Button”。之所以要严格按照这个顺序来写入,是因为接下来要将 step 1 收集到的资源 ID 数组也要写入二进制格式的xml中,保持这个资源ID 和字符串资源池对应字符串的对应关系。 写入的字符串池chunk同样也是具有一个头部的,这个头部的类型为ResStringPool_header,它定义在文件frameworks/base/include/utils/ResourceTypes.h中,如下所示:

    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;
    };
    
    

    --type:等于RES_STRING_POOL_TYPE,描述这是一个字符串资源池。

    --headerSize:等于sizeof(ResStringPool_header),表示头部的大小。

    --size:整个字符串chunk的大小,包括头部headerSize的大小。

    ResStringPool_header的其余成员变量的值如下所示:

    --stringCount:等于字符串的数量。

    --styleCount:等于字符串的样式的数量。

    --flags:等于0、SORTED_FLAG、UTF8_FLAG或者它们的组合值,用来描述字符串资源串的属性,例如,SORTED_FLAG位等于1表示字符串是经过排序的,而UTF8_FLAG位等于1表示字符串是使用UTF8编码的,否则就是UTF16编码的。

    --stringsStart:等于字符串内容块相对于其头部的距离。

    --stylesStart:等于字符串样式块相对于其头部的距离。

    step 6、写入资源ID
    这些收集到的资源ID会作为一个单独的chunk写入到最终的xml二进制文件中。这个chunk位于字符串资源池的后面。它的头部使用ResChunk_header来描述。这个ResChunk_header的各个成员变量的取值如下所示:

    --type:等于RES_XML_RESOURCE_MAP_TYPE,表示这是一个从字符串资源池到资源ID的映射头部。

    --headerSize:等于sizeof(ResChunk_header),表示头部大小。

    --size:等于headerSize的大小再加上sizeof(uint32_t) * count,其中,count为收集到的资源ID的个数。

    以main.xml为例,字符串资源池的第一个字符串为“orientation”,而在资源ID这个chunk中记录的第一个数据为0x010100c4,那么就表示属性名称字符串“orientation”对应的资源ID为0x010100c4。
    step 6、压平xml
    压平xml就是将各个xml元素中的字符串都替换掉。要么被替换成字符串资源池的一个索引,要么是被替换成一个具有类型的其他值。我们以main.xml 为例。

    首先压平的是一个表示命名空间的xml node。这个Xml Node用两个ResXMLTree_node和两个ResXMLTree_namespaceExt来表示,如图所示:

    image

    反正就是哔哩哔哩一大堆的约定协议参数,我就不多说了,感兴趣大家就看老罗的文章研究下。反正就是xml各种稀里糊涂的定规矩,解析什么的。

    第八步、生成资源符号

    这些生成的资源符号为后面生成R.java 做准备。所有的资源项按照类型保存在 ResourceTable对象中,因此 AAPT需要遍历每一个package中的每个tpye,取出每一个 entry。根据这些entry在 type 中的顺序计算他们资源ID。那么就可以生成一个资源符号了。这个资源符号由名称和资源ID组成。

    对于strings.xml文件中名称为“start_in_process”的Entry来说,它是一个类型为string的资源项,假设它出现的次序为第3,那么它的资源符号就等于R.string.start_in_process,对应的资源ID就为0x7f050002,其中,高字节0x7f表示Package ID,次高字节0x05表示string的Type ID,而低两字节0x02就表示“start_in_process”是第三个出现的字符串。

    第九步、生成资源索引表

    经过上面八个步骤,终于获得了资源列表,有了这个 aapt 就可以按照下面的流程生成 资源索引表 resources.arsc。


    image

    感觉又是很多步骤,真实心累的一批哈,看老罗的文章真的是想睡啊~~


    image

    上面我们压平了xml,基本已经完成了一半的任务了,剩下一般就是生成这个resources.arsc 文件,估计又是一大堆的规则。知道大概意思就行了,到了需要的时候再仔细研究就好了。

    step 1、收集类型字符串

    一共有四种类型分别是 drawable 、layout、string 和 id。对应的类型字符串也是drawable 、layout、string 和 id。注意这些字符串是按照报名 package 来收集的。有几个报名就有几组对应的类型字符串。

    step 2、收集资源项 名称 字符串
    比如上面的例子中有 12 个资源项,“icon”、“icon”、“icon”、“main”、“sub”、“app_name”、“sub_activity”、“start_in_process”、“start_in_new_process”、“finish”、“button_start_in_process”和“button_start_in_new_process” 对应的名称字符串也是它们。对的这个也按照 package 来分组。

    step 3、收集资源项 值 字符串
    上一步是 名称 这一回是 值。一共有12个资源项,但是只有10项是具有值字符串的,它们分别是“res/drawable-ldpi/icon.png”、“res/drawable-mdpi/icon.png”、“res/drawable-hdpi/icon.png”、“res/layout/main.xml”、“res/layout/sub.xml”、“Activity”、“Sub Activity”、“Start sub-activity in process”、“Start sub-activity in new process”和“Finish activity”。需要注意的是这些字符串不是按照包 package 来区分的,会被统一收集起来。

    step 4、生成package数据块
    参与编译的每一个 package 的资源项 元信息 都写在一个独立的数据上,这个数据块使用和一个类型为 ResTable_package 的头部来描述。最后是下图这样的形式来的。

    image

    说一个比较不注意的东西点,但是感觉挺重要的点吧,在Android资源中,有一种资源类型成为public,他们一般是定义在 res/values/public.xml 。比如下面这样的,这个public.xml是用来告诉aapt,将类型为string的资源string3的ID 固定为0x7f040001,为什么要固定呢?当我们自己自定义的资源导出来给第三方程序使用的时候,为了保证以后修改这些导出资源时,仍然能保证第三方应用程序的兼容性,就需要给这些导出资源一个固定的资源ID。举个栗子,不然不好理解。那就对比一下有什么区别吧,我们首先建立一个public.xml 里面放一个string3.然后新建两个String,一个string3,一个string1。我们看一下public和没有public的区别吧。

    res/values/public.xml
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <public type="string" name="string3" id="0x7f040001" />
    </resources>
    
    
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="string1">String 1</string>
        <string name="string3">String 3</string>
    </resources>
    

    假设 资源ID是下图这样的,那么第三方引用string3的资源ID永远是0x7f040001。

    
    public final class R {
        // ...
        public static final class string {
            public static final int string1=0x7f040000;
            public static final int string3=0x7f040001;
        }
    }
    

    当有一天,我们增加一个string,按照我们之前所说的他们是按顺序来收集以及分配资源ID的。所以会有所改变。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="string1">String 1</string>
        <string name="string2">String 2</string>
        <string name="string3">String 3</string>
    </resources>
    

    假设string3 没有引入public.xml 中,我们应该猜到是下图这样的

    public final class R {
        // ...
        public static final class string {
            public static final int string1=0x7f040000;
            public static final int string2=0x7f040001;
            public static final int string3=0x7f040002; // New ID! Was 0x7f040001
        }
    }
    

    但是我们放入 public中了,所以它的资源ID是固定的,就是下图这样的。应该挺好理解的。

    public final class R {
        // ...
        public static final class string {
            public static final int string1=0x7f040000;
            public static final int string2=0x7f040002;
            public static final int string3=0x7f040001; // Resource ID from public.xml
        }
    }
    

    需要注意的是我们自己开发应用程序是不需要pubic.xml 这么搞一下的,基本都是内部使用。不会导出来给第三方app使用,只在内部使用的资源,不管它的资源ID怎么变化,我们都能通过R.java 文件定义的常量来引用他们。只有系统定义的资源包才会使用到public.xml 文件。因为它定义的资源需要提供给第三方应用程序使用的。

    其实有一个更常见的情景,我们在反编译的时候,就会看到有这个public.xml 发现R.java的id好像都跑到public.xml里面去了,这是为什么呢?因为我们反编译之后再重新打包,编译,对资源ID会重新编排这是一个随机的过程,但是我们的代码里面还是之前的资源id,那么就乱套了,所以生成一个public.xml保持这些资源ID的固定,感觉又学到知识了,啊哈哈,以前没怎么关心过这个东西,不知道这个还有这一层含义。

    继续我们的 生成package数据块

    1. 写入Package资源项元信息数据块头部
    2. 写入类型字符串资源池
      在上面步骤中我们将每一个package 用到的类型字符串手机起来了,因此直接把他写到package资源项元信息块头部后面的数据块中去。

    3.写入资源名称字符串资源池
    我们已经把资源项名称字符串收集了。因此可以将他们直接写到类型字符串资源池后面的那个数据块中。
    4.写入类型规范数据块
    每一个类型都对应了一个类型规范数据块
    5.写入类型资源项数据块

    step 5、写入资源索引表头部
    step 6、写入资源项的值字符串资源池
    在前三个步骤中已经收集了这些,我们按照相关规则写入就可以了。
    step 7、写入package数据块
    在第四步的时候我们已经收集到了它的信息,同样按规则写入就可以了。

    十、编译AndroidManifest文件
    经过前面九个步骤,终于把应用程序的所有资源项都编译完成了。这个时候就开始讲应用程序的配置文件 AndroidManifest.xml也编译成二进制的xml文件。和之前的道理是一样的。当然aapt也会验证这个文件的完整性和正确性什么的。

    十一、生成R.java
    在第八步的时候,我们已经收集到这些资源ID,这里只是写入到R.java 文件中就好了。

    public final class R {
        ......
     
        public static final class layout {
            public static final int main=0x7f030000;
            public static final int sub=0x7f030001;
        }
     
        ......
    }
    

    十二、打包APK文件
    所有资源文件都编译以及生成完毕之后就可以打包到apk文件中去了。包括以下文件:
    1、assert目录
    2、res目录,但是不包括 res/values 目录,这是因为 res/values 目录下的文件经过编译后,直接写入到了资源项索引表去了。
    3、资源项索引文件 resources.arsc
    当然除了这些资源文件,应用的配置文件 AndroidManifest.xml 以及应用代码文件 class.dex,还有用来描述程序的签名信息的文件,也会被一并打包到 APK中去,这个APK文件可以直接安装了。

    终于看完了,其中最重要的四个要点:

    1、xml 资源文件从文本格式编译成二进制格式的过程

    2、xml 资源文件的二进制格式

    3、项目资源索引文件 resources.arsc 的生成过程

    4、项目资源索引文件 resources.arsc的二进制格式

    好了,这么长的文章,我感觉我是记不住的,那么就提炼一下吧,简化流程。

    [图片上传失败...(image-e4f0c9-1558927882149)]

    闭上眼睛不去看上面这张图,脑子还记的什么呢?突然想起了倚天屠龙记张三丰传授张无忌的桥段。啊全忘记了~呸,咱们这个可不行。既然忘记了,那我们就想想我们要做什么?貌似有两个点

    1、xml 二进制话 更节省空间更快速
    2、resources.arsc 和 R.java 也就是各种收集资源ID

    我们知道这两点就够了,知道了要做什么,然后发散:如何做。

    xml二进制话:其实就是从resources.arsc这个小字典里找到字符串相应的序列号,用0x0011来表示。这个相对来说就简单了一点。

    难点是如何生成resources.arsc,那么我们就想啊,既然要生成这个资源集合文件,那么我们用包名这个唯一标识来创建 资源表吧,那么如何找到包名呢?解析 AndroidManifest.xml,找到了包名,根据包名,创建这个 资源表。

    so:总结一下就是:1、创建资源表

    有了资源表这个文件,那么该怎么往里边填写呢?那么我们就要知道我们收集的是啥?我们收集的资源文件,可以分为 drawable、layout、color、menu、raw、string和xml等,对了还有系统的资源文件,怎么区分系统和我们应用呢?根据包名package啊,然后根据类型type,根据Entry就各种资源ID了。这道怎么区分了,我们就该收集资源了。需要说的是我们不能直接往表里提交啊,万一错了咋办,就需要想用一个临时的容器,然后再往资源表里复制过去。

    首先既然是收集资源,就要有一定的顺序和规矩,不能东一棒槌,西一锥子的,那么我们就以资源的类型来收集。比如 layout 、values、drawable。我们先用drawable来举例子,比如 xh,xxh,xxxh 都有一个icon.png。我们就想啊,怎么来区分呢?都是 icon 啊,那么我们就用组来分吧,比如drawable的容器是set,然后下一个粒度就是组group了,一组icon.png,然后里面的icon.png就是一个单个的文件file,那么怎么怎么知道它的一些特征呢?这个文件就需要一个它的描述文件就是entry,这样就知道这个drawable在xh等其他维度信息。当然别的类型也是如此。

    收集到了临时容器,那么就需要往资源表放,需要注意的是values就先别放了,放其他类型的资源文件,values资源需要编译后再放。

    放的时候和临时容器稍有差异,不过大同小异,每一个资源也是用 entry 表示,根据 package,tpye,configList 来分类。

    至于values资源为什么那么特殊呢?它分为string 和 bag资源,这些bag资源会自定义一些自己的专用值,所以需要编译得到最终的结果才行。对了这个bag资源分为 attr类型资源和id资源类型,怎么区分呢,attr有个内部元数据“^type”。

    好了收集了资源,因为前面values是处于编译阶段,所以也就连带着开始编译xml资源文件。

    解析xml,把里面bag或者其他Values的资源转换成真实的值。就是根据资源的名字,类型,所在包找到对应的资源,根据资源找到它的元数据,就是需要的值了。

    然后就开始压扁xml,也就是二进制他,那么就需要我们首先先收集这个xml里面的资源的属性名称字符串,然后收集其他String字符串。把收集到的这些字符串放到字符串资源池里面。然后根据资源池里作为字典,按照资源序号来重新生成这个xml,也就是二进制xml。就是因为values资源中的bag资源是在values编译的时候就被赋予资源ID的,但是其他资源目前是没有资源ID,只有对应路径的资源名称和资源数据。所以我们需要给资源赋值ID。也就是生成资源符号。就是资源名称+资源ID,为R.java做准备。

    当然有了之前的所有东西,就可以根据规则整成 resources.arsc。各种添添补补什么的。最后生成R.java 文件。最终把生成的resources.arsc、AndroidManifest.xml、res目录(没有values 早被打包到resources.arsc了)、assert还有class.dex 和 签名信息文件 一同打进apk包里。

    好了完工。连粘贴带复制,加上自己手敲终于稍微理清楚了这个关系,不过还需要回头再看一下这个过程才好,明确目的,一步一步根据思路去实现,不纠结具体实现,理清楚核心思想就可以了。

    copy from 以下博客:

    https://www.jianshu.com/p/d487f0aa9818

    https://blog.csdn.net/luoshengyang/article/details/8744683

    https://juejin.im/entry/58b78d1b61ff4b006cd47e5b

    http://www.10tiao.com/html/597/201808/2651943416/1.html

    https://www.jianshu.com/p/3cc131db2002

    相关文章

      网友评论

          本文标题:Android 编译打包的那些疑问

          本文链接:https://www.haomeiwen.com/subject/onictctx.html