Android資源編譯和打包過程分析

這一篇是咱們Android熱修復學習深刻分析的第一篇。 學習總綱計劃能夠看上一篇文章總綱 首先咱們先來分析資源修復相關知識。資源修復的過程基本能夠分析爲這麼一個過程: 老包(線上出現bug須要修復的那個apk包)打包成apk的時候把它須要的資源都打包進去了,而後新的補丁包加入了增長的資源或者須要替換的資源,apk在運行時讀取相關資源的時候進行了增長或者替換相關的操做。因此今天咱們先來分析資源編譯和打包的整個過程。java

簡介

咱們都知道apk實際上是一個壓縮包,我將一個平時開發的apk解壓獲得以下目錄: android

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常量。bash

那麼資源索引表resources.arsc跟R.java文件打開都是什麼樣的呢?接下來咱們就先來看看這兩個文件裏最終形態究竟是怎麼樣的,根據最後展現給咱們的樣子再去反推過程會更加容易理解。微信

解析resources.arsc文件內容

注意:本篇文章Android 源碼都出自Android 9.0, 這裏,我新建了一個項目,加了各類資源: 數據結構

圖2
而後打包出了一個apk,拿到他的resources.arsc文件對其進行解析:
圖3

先來看網上這張神圖: app

圖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的大小ide

接着咱們再來看下整個資源索引表的頭部信息,也就是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數據塊其實包括了: 一、header 二、資源類型字符串池,也就是type string pool 三、資源項名稱字符串池,也就是key string pool 四、類型規範數據塊,也就是type specification 五、資源類型項數據塊,也便是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

一、解析AndroidManifest.xml

主要作一些檢查,獲取package ID,minSdkVersion,uses-sdk等屬性。

二、添加被引用資源包

上面咱們也講到了,一般在編譯一個apk的時候至少會牽扯到兩個資源包,一個是被引用的系統資源包,裏面包含了不少系統級的,就好比一個LinearLayout,有layout_width,layout_height,layout_oritation等屬性。 這裏有一點要注意,這裏有一個處理重疊包的過程,其實也就是上面咱們講到的entryCount(本類型也就是名稱相同的資源個數),若是名稱相同,則使用重疊包。

三、收集資源文件

這裏aapt會建立一個AaptAssets對象,將當前須要編譯的資源文件根據類別保存下來。注意,這裏的資源文件指的是除了values資源外的資源,由於values資源是在編譯的時候進行收集的。

四、把收集到的資源文件保存到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資源比較特別,須要進行編譯之後纔會保存。

五、編譯values資源

values下的資源都是諸如strings/colors/ids這種輕量級的資源,這些資源都是在編譯的時候進行收集的。

六、給bag資源分配id

bag資源是什麼,bag資源就是這類資源在賦值的時候,不能隨便賦值,只能從事先定義好的值中選取一個賦值。很像枚舉,好比layout_oritation這種,如attr資源。這一步咱們會給bag資源分配資源id。能夠理解成給枚舉的兩個值分配資源id,固然這個不是枚舉。

七、編譯xml文件

ok,前面的步驟主要是爲了給咱們編譯xml文件作準備,如今開始,咱們就能夠編譯xml文件了。這裏,程序會對layouts,anims,animators等文件逐一調用ResourceTable.cpp的以下方法進行編譯:

status_t compileXmlFile(const sp<AaptAssets>& assets,
                        const sp<AaptFile>& target,
                        ResourceTable* table,
                        int options);
複製代碼

內部流程能夠分爲: 一、解析xml文件: 這一步主要是爲了將xml文件轉化爲一系列樹形結構XmlNode來表示。

二、賦予屬性名稱id: 給每個資源的屬性名稱賦予id。就好比一個最基本的button,他有layout_width和layout_height兩個屬性,這兩個屬性都屬於bag資源,在上一步中咱們已經把他們編譯了,這一步就是把編譯後的id賦值給這個button。 每個xml都是從根節點開始賦予屬性名稱id,直到該文件下全部節點都有屬性id了爲止。

三、解析屬性值 這一步是第二部的深化,第二部咱們對layout_width和layout_height這兩個屬性名稱賦予了id,這一步咱們將對其值進行解析。仍然是這個button,咱們將對match_parent或者wrap_content進行解析。

四、扁平化爲二進制文件 將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文件,也就是把裏面的元素都替換掉,徹底變成二進制文件。

八、給資源生成資源ID

這裏就是給資源生成資源id,id是一個32位數字,用十六進制來表示就是0XPPTTEEEE。 PP爲package id,也就是上面咱們提到的ResTable_package數據結構中的id; TT位type id,也就是咱們上面提到的ResTable_typeSpec數據結構中的id; EEEE爲entry id,每一個entry表示一個資源項,按照前後順序自動排列,這裏須要注意,是根據順序自動排列,由於這個entry id牽扯到熱修復更新資源下面的內容,因此這裏須要特別注意,以後會提到,這裏就不展開了。

九、根據資源ID生成資源索引表

這裏咱們將生成resources.arsc步驟拆解以下:

  1. 以package爲單位,收集類別字符串,例如「drawable」,「string」等。
  2. 以package爲單位,收集資源項名稱字符串,就好比圖2咱們建的那個項目,以strings.xml爲例,這裏咱們就收集了"app_name","jjq","hahahaha"三個字符串。
  3. 全部資源項值字符串,再以圖2項目爲例,就是"ResourceDemo","好帥"和「哈哈哈哈哈哈」;
  4. 生成package數據塊,package的數據結構上面解析的時候已經講過了,這裏其實就是把步驟一、二、3獲取到的資源一個一個填進去。
  5. 寫入資源索引表頭部,也就是ResTable_header。
  6. 寫入字符串資源池,由於數據都準備好了,因此這裏直接寫就行了
  7. 寫入package,第4步中已經生成好了

十、編譯AndroidManifest.xml

如今咱們能夠編譯AndroidManifest.xml文件,將其編譯成二進制文件。

十一、生成R.java

這裏咱們已經知道了全部的資源項以及其id,這裏咱們就能夠把他們都寫到R.java文件裏了。。。這裏須要注意的是,R.java裏每個資源類別對應一個內部類,就像這樣:

圖11
圖片上舉例了就是兩個anim,attr兩個類別對應的內部類。

十二、打包到apk裏

接下來就是打包到apk裏了,這裏咱們會將assets文件目錄,res目錄下但不包括res/values目錄下的資源文件,resources.arsc資源索引文件打包進apk裏。

至此,整個Android 資源編譯和打包過程就分析完了。。。。。

有了這個基礎,接下來咱們就能夠研究apk運行的時候是如何讀取最適合的,相對應的資源文件的。知道了這個過程之後,咱們就能夠深刻探索Android熱修復如何才能作到運行的時候去替換資源文件。

本系列目錄: Android熱修復原理簡要介紹和學習計劃

參考文章: 一、《深刻探索Android熱修復技術原理》 二、老羅:Android應用程序資源的編譯和打包過程分析 三、blog.csdn.net/jiangwei091… 四、blog.zhaiyifan.cn/2016/02/13/…

我的微信公共號已上線,歡迎關注:

在這裏插入圖片描述
相關文章
相關標籤/搜索