天天看點

Android應用程式資源的編譯和打包過程分析

        我們知道,在一個APK檔案中,除了有代碼檔案之外,還有很多資源檔案。這些資源檔案是通過Android資源打包工具aapt(Android Asset Package Tool)打包到APK檔案裡面的。在打包之前,大部分文本格式的XML資源檔案還會被編譯成二進制格式的XML資源檔案。在本文中,我們就詳細分析XML資源檔案的編譯和打包過程,為後面深入了解Android系統的資源管理架構打下堅實的基礎。

《Android系統源代碼情景分析》一書正在進擊的程式員網(http://0xcc0xcd.com)中連載,點選進入!

        在前面Android資源管理架構(Asset Manager)簡要介紹和學習計劃一文中提到,隻有那些類型為res/animator、res/anim、res/color、res/drawable(非Bitmap檔案,即非.png、.9.png、.jpg、.gif檔案)、res/layout、res/menu、res/values和res/xml的資源檔案均會從文本格式的XML檔案編譯成二進制格式的XML檔案,如圖1所示:

Android應用程式資源的編譯和打包過程分析

圖1 Android應用程式資源的編譯和打包過程

        這些XML資源檔案之所要從文本格式編譯成二進制格式,是因為:

        1. 二進制格式的XML檔案占用空間更小。這是由于所有XML元素的标簽、屬性名稱、屬性值和内容所涉及到的字元串都會被統一收集到一個字元串資源池中去,并且會去重。有了這個字元串資源池,原來使用字元串的地方就會被替換成一個索引到字元串資源池的整數值,進而可以減少檔案的大小。

        2. 二進制格式的XML檔案解析速度更快。這是由于二進制格式的XML元素裡面不再包含有字元串值,是以就避免了進行字元串解析,進而提高速度。

        将XML資源檔案從文本格式編譯成二進制格式解決了空間占用以及解析效率的問題,但是對于Android資源管理架構來說,這隻是完成了其中的一部分工作。Android資源管理架構的另外一個重要任務就是要根據資源ID來快速找到對應的資源。

        在前面Android資源管理架構(Asset Manager)簡要介紹和學習計劃一文中提到,為了使得一個應用程式能夠在運作時同時支援不同的大小和密度的螢幕,以及支援國際化,即支援不同的國家地區和語言,Android應用程式資源的組織方式有18個次元,每一個次元都代表一個配置資訊,進而可以使得應用程式能夠根據裝置的目前配置資訊來找到最比對的資源來展現在UI上,進而提高使用者體驗。

        由于Android應用程式資源的組織方式可以達到18個次元,是以就要求Android資源管理架構能夠快速定位最比對裝置目前配置資訊的資源來展現在UI上,否則的話,就會影響使用者體驗。為了支援Android資源管理架構快速定位最比對資源,Android資源打包工具aapt在編譯和打包資源的過程中,會執行以下兩個額外的操作:

        1. 賦予每一個非assets資源一個ID值,這些ID值以常量的形式定義在一個R.java檔案中。

        2. 生成一個resources.arsc檔案,用來描述那些具有ID值的資源的配置資訊,它的内容就相當于是一個資源索引表。

        有了資源ID以及資源索引表之後,Android資源管理架構就可以迅速将根據裝置目前配置資訊來定位最比對的資源了。接下來我們在分析Android應用程式資源的編譯和打包過程中,就主要關注XML資源的編譯過程、資源ID檔案R.java的生成過程以及資源索引表檔案resources.arsc的生成過程。

        Android資源打包工具在編譯應用程式資源之前,會建立一個資源表。這個資源表使用一個ResourceTable對象來描述,當應用程式資源編譯完成之後,它就會包含所有資源的資訊。有了這個資源表之後, Android資源打包工具就可以根據它的内容來生成資源索引表檔案resources.arsc了。

        接下來,我們就通過ResourceTable類的實作來先大概了解資源表裡面都有些什麼東西,如圖2所示:

Android應用程式資源的編譯和打包過程分析

圖2 ResourceTable的實作

        ResourceTable類用來總體描述一個資源表,它的重要成員變量的含義如下所示:

        --mAssetsPackage:表示目前正在編譯的資源的包名稱。

        --mPackages:表示目前正在編譯的資源包,每一個包都用一個Package對象來描述。例如,一般我們在編譯應用程式資源時,都會引用系統預先編譯好的資源包,這樣目前正在編譯的資源包除了目标應用程式資源包之外,就還有預先編譯好的系統資源包。

        --mOrderedPackages:和mPackages一樣,也是表示目前正在編譯的資源包,不過它們是以Package ID從小到大的順序儲存在一個Vector裡面的,而mPackages是一個以Package Name為Key的DefaultKeyedVector。

        --mAssets:表示目前編譯的資源目錄,它指向的是一個AaptAssets對象。

        Package類用來描述一個包,這個包可以是一個被引用的包,即一個預先編譯好的包,也可以是一個正在編譯的包,它的重要成員變量的含義如下所示:

        --mName:表示包的名稱。

        --mTypes:表示包含的資源的類型,每一個類型都用一個Type對象來描述。資源的類型就是指animimator、anim、color、drawable、layout、menu和values等。

        --mOrderedTypes:和mTypes一樣,也是表示包含的資源的類型,不過它們是Type ID從小到大的順序儲存在一個Vector裡面的,而mTypes是一個以Type Name為Key的DefaultKeyedVector。

        Type類用來描述一個資源類型,它的重要成員變量的含義如下所示:

         --mName:表示資源類型名稱。

         --mConfigs:表示包含的資源配置項清單,每一個配置項清單都包含了一系列同名的資源,使用一個ConfigList來描述。例如,假設有main.xml和sub.xml兩個layout類型的資源,那麼main.xml和sub.xml都分别對應有一個ConfigList。

         --mOrderedConfigs:和mConfigs一樣,也是表示包含的資源配置項,不過它們是以Entry ID從小到大的順序儲存在一個Vector裡面的,而mConfigs是以Entry Name來Key的DefaultKeyedVector。

         --mUniqueConfigs:表示包含的不同資源配置資訊的個數。我們可以将mConfigs和mOrderedConfigs看作是按照名稱的不同來劃分資源項,而将mUniqueConfigs看作是按照配置資訊的不同來劃分資源項。

        ConfigList用來描述一個資源配置項清單,它的重要成員變量的含義如下所示:

        --mName:表示資源項名稱,也稱為Entry Name。

        --mEntries:表示包含的資源項,每一個資源項都用一個Entry對象來描述,并且以一個對應的ConfigDescription為Key儲存在一個DefaultKeyedVector中。例如,假設有一個名稱為icon.png的drawable資源,有三種不同的配置,分别是ldpi、mdpi和hdpi,那麼以icon.png為名稱的資源就對應有三個項。

        Entry類用來描述一個資源項,它的重要成員變量的含義如下所示:

        --mName:表示資源名稱。

        --mItem:表示資源資料,用一個Item對象來描述。

        Item類用來描述一個資源項資料,它的重要成員變量的含義如下所示:

        --value:表示資源項的原始值,它是一個字元串。

        --parsedValue:表示資源項原始值經過解析後得到的結構化的資源值,使用一個Res_Value對象來描述。例如,一個整數類型的資源項的原始值為“12345”,經過解析後,就得到一個大小為12345的整數類型的資源項。

        ConfigDescription類是從ResTable_config類繼承下來的,用來描述一個資源配置資訊。ResTable_config類的成員變量imsi、locale、screenType、input、screenSize、version和screenConfig對應的實際上就是在前面Android資源管理架構(Asset Manager)簡要介紹和學習計劃一文提到的18個資源次元。

        前面提到,目前正在編譯的資源目錄是使用一個AaptAssets對象來描述的,它的實作如圖3所示:

Android應用程式資源的編譯和打包過程分析

圖3 AaptAssets類的實作

        AaptAssets類的重要成員變量的含義如下所示:

        --mPackage:表示目前正在編譯的資源的包名稱。

        --mRes:表示所包含的資源類型集,每一個資源類型都使用一個ResourceTypeSet來描述,并且以Type Name為Key儲存在一個KeyedVector中。

        --mHaveIncludedAssets:表示是否有引用包。

        --mIncludedAssets:指向的是一個AssetManager,用來解析引用包。引用包都是一些預編譯好的資源包,它們需要通過AssetManager來解析。事實上,Android應用程式在運作的過程中,也是通過AssetManager來解析資源的。

        --mOverlay:表示目前正在編譯的資源的重疊包。重疊包是什麼概念呢?假設我們正在編譯的是Package-1,這時候我們可以設定另外一個Package-2,用來告訴aapt,如果Package-2定義有和Package-1一樣的資源,那麼就用定義在Package-2的資源來替換掉定義在Package-1的資源。通過這種Overlay機制,我們就可以對資源進行定制,而又不失一般性。

        ResourceTypeSet類實際上描述的是一個類型為AaptGroup的KeyedVector,并且這個KeyedVector是以AaptGroup Name為Key的。AaptGroup類描述的是一組同名的資源,類似于前面所描述的ConfigList,它有一個重要的成員變量mFiles,裡面儲存的就是一系列同名的資源檔案。每一個資源檔案都是用一個AaptFile對象來描述的,并且以一個AaptGroupEntry為Key儲存在一個DefaultKeyedVector中。

        AaptFile類的重要成員變量的含義如下所示:

        --mPath:表示資源檔案路徑。

        --mGroupEntry:表示資源檔案對應的配置資訊,使用一個AaptGroupEntry對象來描述。

        --mResourceType:表示資源類型名稱。

        --mData:表示資源檔案編譯後得到的二進制資料。

        --mDataSize:表示資源檔案編譯後得到的二進制資料的大小。

        AaptGroupEntry類的作用類似前面所描述的ResTable_config,它的成員變量mcc、mnc、locale、vendor、screenLayoutSize、screenLayoutLong、orientation、uiModeType、uiModeNight、density、tounscreen、keysHidden、keyboard、navHidden、navigation、screenSize和version對應的實際上就是在前面Android資源管理架構(Asset Manager)簡要介紹和學習計劃一文提到的18個資源次元。

        了解了ResourceTable類和AaptAssets類的實作之後,我們就可以開始分析Android資源打包工具的執行過程了,如圖4所示:

Android應用程式資源的編譯和打包過程分析

圖4 Android資源打包工具的執行過程

        假設我們目前要編譯的應用程式資源目錄結構如下所示:

project
  --AndroidManifest.xml
  --res
    --drawable-ldpi
      --icon.png
    --drawable-mdpi
      --icon.png
    --drawable-hdpi
      --icon.png
    --layout
      --main.xml
      --sub.xml
    --values
      --strings.xml
           

         接下來,我們就按照圖4所示的步驟來分析上述應用程式資源的編譯和打包過程。

         一. 解析AndroidManifest.xml

         解析AndroidManifest.xml是為了獲得要編譯資源的應用程式的包名稱。我們知道,在AndroidManifest.xml檔案中,manifest标簽的package屬性的值描述的就是應用程式的包名稱。有了這個包名稱之後,就可以建立資源表了,即建立一個ResourceTable對象。

         二. 添加被引用資源包

         Android系統定義了一套通用資源,這些資源可以被應用程式引用。例如,我們在XML布局檔案中指定一個LinearLayout的android:orientation屬性的值為“vertical”時,這個“vertical”實際上就是在系統資源包裡面定義的一個值。

        在Android源代碼工程環境中,Android系統提供的資源經過編譯後,就位于out/target/common/obj/APPS/framework-res_intermediates/package-export.apk檔案中,是以,在Android源代碼工程環境中編譯的應用程式資源,都會引用到這個package-export.apk。

        從上面的分析就可以看出,我們在編譯一個Android應用程式的資源的時候,至少會涉及到兩個包,其中一個被引用的系統資源包,另外一個就是目前正在編譯的應用程式資源包。每一個包都可以定義自己的資源,同時它也可以引用其它包的資源。那麼,一個包是通過什麼方式來引用其它包的資源的呢?這就是我們熟悉的資源ID了。資源ID是一個4位元組的無符号整數,其中,最高位元組表示Package ID,次高位元組表示Type ID,最低兩位元組表示Entry ID。

        Package ID相當于是一個命名空間,限定資源的來源。Android系統目前定義了兩個資源指令空間,其中一個系統資源指令空間,它的Package ID等于0x01,另外一個是應用程式資源指令空間,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之間的Package ID都是合法的,而在這個範圍之外的都是非法的Package ID。前面提到的系統資源包package-export.apk的Package ID就等于0x01,而我們在應用程式中定義的資源的Package ID的值都等于0x7f,這一點可以通過生成的R.java檔案來驗證。

        Type ID是指資源的類型ID。資源的類型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若幹種,每一種都會被賦予一個ID。

        Entry ID是指每一個資源在其所屬的資源類型中所出現的次序。注意,不同類型的資源的Entry ID有可能是相同的,但是由于它們的類型不同,我們仍然可以通過其資源ID來差別開來。

        關于資源ID的更多描述,以及資源的引用關系,可以參考frameworks/base/libs/utils目錄下的README檔案。

        三. 收集資源檔案

        在編譯應用程式資源之前,Android資源打包工具aapt會建立一個AaptAssets對象,用來收集目前需要編譯的資源檔案。這些需要編譯的資源檔案就儲存在AaptAssets類的成員變量mRes中,如下所示:

class AaptAssets : public AaptDir
{
    ......

private:
    ......

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

        AaptAssets類定義在檔案frameworks/base/tools/aapt/AaptAssets.h中。

        AaptAssets類的成員變量mRes是一個類型為ResourceTypeSet的KeyedVector,這個KeyedVector的Key就是資源的類型名稱。由此就可知,收集到資源檔案是按照類型來儲存的。例如,對于我們在這篇文章中要用到的例子,一共有三種類型的資源,分别是drawable、layout和values,于是,就對應有三個ResourceTypeSet。

        從前面的圖3可以看出,ResourceTypeSet類本身描述的也是一個KeyedVector,不過它裡面儲存的是一系列有着相同檔案名的AaptGroup。例如,對于我們在這篇文章中要用到的例子:

        1. 類型為drawable的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。這兩個AaptGroup都是隻包含了一個AaptFile,分别是res/layout/main.xml和res/layout/sub.xml。這兩個AaptFile同樣是分别對應有一個AaptGroupEntry,不過這兩個AaptGroupEntry描述的資源配置資訊都是屬于default的。

        3. 類型為values的ResourceTypeSet隻有一個AaptGroup,它的名稱為strings.xml。這個AaptGroup隻包含了一個AaptFile,即res/values/strings.xml。這個AaptFile也對應有一個AaptGroupEntry,這個AaptGroupEntry描述的資源配置資訊也是屬于default的。

        四. 将收集到的資源增加到資源表

        前面收集到的資源隻是儲存在一個AaptAssets對象中,這一步需要将這些資源同時增加到一個資源表中去,即增加到前面所建立的一個ResourceTable對象中去,因為最後我們需要根據這個ResourceTable來生成資源索引表,即生成resources.arsc檔案。

        注意,這一步收集到資源表的資源是不包括values類型的資源的。類型為values的資源比較特殊,它們要經過編譯之後,才會添加到資源表中去。這個過程我們後面再描述。

        從前面的圖2可以看出,在ResourceTable類中,每一個資源都是分别用一個Entry對象來描述的,這些Entry分别按照Pacakge、Type和ConfigList來分類儲存。例如,對于我們在這篇文章中要用到的例子,假設它的包名為“shy.luo.activity”,那麼在ResourceTable類的成員變量mPackages和mOrderedPackages中,就會分别儲存有一個名稱為“shy.luo.activity”的Package,如下所示:

class ResourceTable : public ResTable::Accessor
{
    ......

private:
    ......

    DefaultKeyedVector<String16, sp<Package> > mPackages;
    Vector<sp<Package> > mOrderedPackages;
   
    ......
};
           

       ResourceTable類定義在檔案frameworks/base/tools/aapt/ResourceTable.h中。

       在這個名稱為“shy.luo.activity”的Package中,分别包含有drawable和layout兩種類型的資源,每一種類型使用一個Type對象來描述,其中:

       1. 類型為drawable的Type包含有一個ConfigList。這個ConfigList的名稱為icon.png,包含有三個Entry,分别為res/drawable-ldip/icon.png、res/drawable-mdip/icon.png和res/drawable-hdip/icon.png。每一個Entry都對應有一個ConfigDescription,用來描述不同的資源配置資訊,即分别用來描述ldpi、mdpi和hdpi三種不同的螢幕密度。

       2. 類型為layout的Type包含有兩個ConfigList。這兩個ConfigList的名稱分别為main.xml和sub.xml。名稱為main.xml的ConfigList包含有一個Entry,即res/layout/main.xml。名稱為sub.xml的ConfigList包含有一個Entry,即res/layout/sub/xml。

       上述得到的五個Entry分别對應有五個Item,它們的對應關系以及内容如下圖5所示:

Android應用程式資源的編譯和打包過程分析

圖5 收集到的drawable和layout資源項清單

        五. 編譯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,這個Type有五個ConfigList。這五個ConfigList的名稱分别為“app_name”、“sub_activity”、“start_in_process”、“start_in_new_process”和“finish”,每一個ConfigList又分别含有一個Entry。

        上述得到的五個Entry分别對應有五個Item,它們的對應關系以及内容如圖6所示:

Android應用程式資源的編譯和打包過程分析

圖6 收集到的string資源項清單

        六. 給Bag資源配置設定ID

        類型為values的資源除了是string之外,還有其它很多類型的資源,其中有一些比較特殊,如bag、style、plurals和array類的資源。這些資源會給自己定義一些專用的值,這些帶有專用值的資源就統稱為Bag資源。例如,Android系統提供的android:orientation屬性的取值範圍為{“vertical”、“horizontal”},就相當于是定義了vertical和horizontal兩個Bag。

        在繼續編譯其它非values的資源之前,我們需要給之前收集到的Bag資源配置設定資源ID,因為它們可能會被其它非values類資源引用到。假設在res/values目錄下,有一個attrs.xml檔案,它的内容如下所示:

<?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>
           

        這個檔案定義了一個名稱為“custom_orientation”的屬性,它是一個枚舉類型的屬性,可以取值為“custom_vertical”或者“custom_horizontal”。Android資源打包工具aapt在編譯這個檔案的時候,就會生成以下三個Entry,如圖7所示:

Android應用程式資源的編譯和打包過程分析

圖7 收集到的Bag資源項清單

        上述三個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”和TYPE_ENUM,用來表示它描述的是一個枚舉類型的屬性。注意,所有名稱以“^”開頭的bag都是表示一個内部中繼資料。

        對于Bag資源來說,這一步需要給它們的中繼資料項配置設定資源ID,也就是給它們的bag配置設定資源ID。例如,對于上述的custom_orientation來說,我們需要給它的^type、custom_vertical和custom_horizontal配置設定資源ID,其中,^type配置設定到的是attr類型的資源ID,而custom_vertical和custom_horizontal配置設定到的是id類型的資源ID。

        七. 編譯Xml資源檔案

        前面的六步操作為編譯Xml資源檔案準備好了所有的素材,是以,現在就開始要編譯Xml資源檔案了。除了values類型的資源檔案,其它所有的Xml資源檔案都需要編譯。這裡我們隻挑layout類型的資源檔案來說明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>
           

        Xml資源檔案main.xml的編譯過程如圖8所示:

Android應用程式資源的編譯和打包過程分析

圖8 Xml資源檔案的編譯過程

        1. 解析Xml檔案

         解析Xml檔案是為了可以在記憶體中用一系列樹形結構的XMLNode來表示它。XMLNode類的定義在檔案frameworks/base/tools/aapt/XMLNode.h中,如下所示:

class XMLNode : public RefBase
{
    ......

private:
    ......

    String16 mElementName;
    Vector<sp<XMLNode> > mChildren;
    Vector<attribute_entry> mAttributes;
    ......
    String16 mChars;
    ......
};
           

        每一個XMLNode都表示一個Xml元素,其中:

        --mElementName,表示Xml元素标簽。

        --mChars,表示Xml元素的文本内容。

        --mAttributes,表示Xml元素的屬性清單。

        --mChildren,表示Xml元素的子元素。

        Xml檔案解析完成之後,就可以得到一個用來描述根節點的XMLNode,接下來就可以通過這個根節點來完成其它的編譯操作。

        2. 賦予屬性名稱資源ID

        這一步實際上就是給每一個Xml元素的屬性名稱都賦予資源ID。例如,對于main.xml檔案的根節點LinearLayout來說,就是要分别給它的屬性名稱“android:orientation”、“android:layout_width”、“android:layout_height”和“android:gravity”賦予一個資源ID。注意,上述這些屬性都是在系統資源包裡面定義的,是以,Android資源打包工具首先是要在系統資源包裡面找到這些名稱所對應的資源ID,然後才能賦給main.xml檔案的根節點LinearLayout。

        對于系統資源包來說,“android:orientation”、“android:layout_width”、“android:layout_height”和“android:gravity”等這些屬性名稱是它定義的一系列Bag資源,在它被編譯的時候,就已經配置設定好資源ID了,就如上面的第六步操作所示。

        每一個Xml檔案都是從根節點開始給屬性名稱賦予資源ID,然後再給遞歸給每一個子節點的屬性名稱賦予資源ID,直到每一個節點的屬性名稱都獲得了資源ID為止。

        3. 解析屬性值

        上一步是對Xml元素的屬性的名稱進行解析,這一步是對Xml元素的屬性的值進行解析。例如,對于對于main.xml檔案的根節點LinearLayout來說,前面我們已經給它的屬性android:orientation的名稱賦予了一個資源ID,這裡就要給它的值“vertical”進行解析。

        前面提到,android:orientation是在系統資源包定義的一個Bag資源,這個Bag資源配置設定有資源ID,而且會指定有中繼資料,也就是它可以取哪些值。對于android:orientation來說,它的合法取值就為“horizontal”或者“vertical”。在系統資源包中,“horizontal”或者“vertical”也同樣是一個Bag資源,它們的值分别被定義為0和1。

        Android資源打包工具是如何找到main.xml檔案的根節點LinearLayout的屬性android:orientation的字元串值“vertical”所對應的整數值1的呢?假設在上一步中,從系統資源包找到“android:orientation”的資源ID為0x010100c4,那麼Android資源打包工具就會通過這個資源ID找到它的中繼資料,也就是兩個名稱分别為“horizontal”和“vertical”的bag,接着就根據字元串比對到名稱“vertical”的bag,最後就可以将這個bag的值1作為解析結果了。

        注意,對于引用類型的屬性值,要進行一些額外的處理。例如,對于main.xml檔案的第一個Button節點的android:id屬性值“@+id/button_start_in_process”,其中,“@”表示後面描述的屬性是引用類型的,“+”表示如果該引用不存在,那麼就建立一個,“id”表示引用的資源類型是id,“button_start_in_process”表示引用的名稱。實際上,在"id"前面,還可以指定一個包名,例如,将main.xml檔案的第一個Button節點的android:id屬性值指定為“@+[package:]id/button_start_in_process” 。如果沒有指定包名的話,那麼就會預設在目前編譯的包裡面查找button_start_in_process這個引用。由于前面指有“+”符号,是以,如果在指定的包裡面找不到button_start_in_process這個引用的話,那麼就會在該包裡面建立一個新的。無論button_start_in_process在指定的包裡面原來就存在的,還是建立的,最終Android資源打包工具都是将它的資源ID作為解析結果。

        在我們這個情景中,在解析main.xml檔案的兩個Button節點的android:id屬性值“@+id/button_start_in_process”和“@+id/button_start_in_new_process”時,目前正在編譯的資源包沒有包含有相應的引用的,是以,Android資源打包工具就會在目前正在編譯的資源包裡面增加兩個類型為id的Entry,如圖9所示:

Android應用程式資源的編譯和打包過程分析

圖9 增加兩個類型為id的資源項

        此外,對于main.xml檔案的兩個Button節點的android:text屬性值“@string/start_in_process”和“@string/start_in_new_process”,它們分别表示引用的是目前正在編譯的資源包的名稱分别為“start_in_process”和“start_in_new_process”的string資源。這兩個string資源在前面的第五步操作中已經編譯過了,是以,這裡就可以直接獲得它們的資源ID。

        注意,一個資源項一旦建立之後,要獲得它的資源ID是很容易的,因為它的Package ID、Type ID和Entry ID都是已知的。

        4. 壓平Xml檔案

        經過前面的三步操作之後,所需要的基本材料都已經準備好了,接下來就可以對Xml檔案的内容進行扁平化處理了,實際上就是将Xml檔案從文本格式轉換為二進制格式,這個過程如圖10所示:

Android應用程式資源的編譯和打包過程分析

圖10 壓平Xml檔案

        将Xml檔案從文本格式轉換為二進制格式可以劃分為六個步驟,接下來我們就詳細分析每一個步驟。

        Step 1. 收集有資源ID的屬性的名稱字元串

        這一步除了收集那些具有資源ID的Xml元素屬性的名稱字元串之外,還會将對應的資源ID收集起來放在一個數組中。這裡收集到的屬性名稱字元串儲存在一個字元串資源池中,它們與收集到的資源ID數組是一一對應的。

        對于main.xml檔案來說,具有資源ID的Xml元素屬性的名稱字元串有“orientation”、“layout_width”、“layout_height”、“gravity”、“id”和“text”,假設它們對應的資源ID分别為0x010100c4、0x010100f4、0x010100f5、0x010100af、0x010100d0和0x0101014f,那麼最終得到的字元串資源池的前6個位置和資源ID數組的對應關系如圖11所示:

Android應用程式資源的編譯和打包過程分析

圖11 屬性名稱字元串與屬性資源ID的對應關系

        Step 2. 收集其它字元串

        這一步收集的是Xml檔案中的其它所有字元串。由于在前面的Step 1中,那些具有資源ID的Xml元素屬性的名稱字元串已經被收集過了,是以,它們在一步中不會被重複收集。對于main.xml檔案來說,這一步收集到的字元串如圖12所示:

Android應用程式資源的編譯和打包過程分析

圖12 其它字元串

        其中,“android”是android命名空間字首,“http://schemas.android.com/apk/res/android”是android命名空間uri,“LinearLayout”是LinearLayout元素的标簽,“Button”是Button元素的标簽。

        Step 3. 寫入Xml檔案頭

        最終編譯出來的Xml二進制檔案是一系列的chunk組成的,每一個chunk都有一個頭部,用來描述chunk的元資訊。同時,整個Xml二進制檔案又可以看成一塊總的chunk,它有一個類型為ResXMLTree_header的頭部。

        ResXMLTree_header定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

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

/**
 * XML tree header.  This appears at the front of an XML tree,
 * describing its content.  It is followed by a flat array of
 * ResXMLTree_node structures; the hierarchy of the XML document
 * is described by the occurrance of RES_XML_START_ELEMENT_TYPE
 * and corresponding RES_XML_END_ELEMENT_TYPE nodes in the array.
 */
struct ResXMLTree_header
{
    struct ResChunk_header header;
};
           

        ResXMLTree_header内嵌有一個類型為ResChunk_header的頭部。事實上,每一種頭部類型都會内嵌有一個類型為ResChunk_header的基礎頭部,并且這個ResChunk_header都是作為第一個成員變量出現的。這樣在解析二進制Xml檔案的時候,隻需要讀出前面大小為sizeof(ResChunk_header)的資料塊,并且通過識别其中的type值,就可以知道實際正在處理的chunk的具體類型。

        對于ResXMLTree_header頭部來說,内嵌在它裡面的ResChunk_header的成員變量的值如下所示:

        --type:等于RES_XML_TYPE,描述這是一個Xml檔案頭部。

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

        --size:等于整個二進制Xml檔案的大小,包括頭部headerSize的大小。

        Step 4. 寫入字元串資源池

        原來定義在Xml檔案中的字元串已經在Step 1和Step 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中,如下所示:

/**
 * Definition for a pool of strings.  The data of this chunk is an
 * array of uint32_t providing indices into the pool, relative to
 * stringsStart.  At stringsStart are all of the UTF-16 strings
 * concatenated together; each starts with a uint16_t of the string's
 * length and each ends with a 0x0000 terminator.  If a string is >
 * 32767 characters, the high bit of the length is set meaning to take
 * those 15 bits as a high word and it will be followed by another
 * uint16_t containing the low word.
 *
 * If styleCount is not zero, then immediately following the array of
 * uint32_t indices into the string table is another array of indices
 * into a style table starting at stylesStart.  Each entry in the
 * style table is an array of ResStringPool_span structures.
 */
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;
};
           

        内嵌在ResStringPool_header裡面的ResChunk_header的成員變量的值如下所示:

        --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:等于字元串樣式塊相對于其頭部的距離。

        無論是UTF8,還是UTF16的字元串編碼,每一個字元串的前面都有2個位元組表示其長度,而且後面以一個NULL字元結束。對于UTF8編碼的字元串來說,NULL字元使用一個位元組的0x00來表示,而對于UTF16編碼的字元串來說,NULL字元使用兩個位元組的0x0000來表示。

        如果一個字元串的長度超過32767,那麼就會使用更多的位元組來表示。假設字元串的長度超過32767,那麼前兩個位元組的最高位就會等于0,表示接下來的兩個位元組仍然是用來表示字元串長度的,并且前兩個字表示高16位,而後兩個位元組表示低16位。

        除了ResStringPool_header頭部、字元串内容塊和字元串樣式内容塊之外,還有兩個偏移數組,分别是字元串偏移數組和字元串樣式偏移數組,這兩個偏移數組的大小就分别等于字元串的數量stringCount和styleCount的值,而每一個元素都是一個無符号整數。整個字元中資源池的組成就如圖13所示:

Android應用程式資源的編譯和打包過程分析

圖13 字元串資源池結構

        注意,字元串偏移數組和字元串樣式偏移數組的值分别是相對于stringStart和styleStart而言的。在解析二進制Xml檔案的時候,通過這兩個偏移數組以及stringsStart和stylesStart的值就可以迅速地定位到第i個字元串。

        接下來,我們就重點說說什麼是字元串樣式。假設有一個字元串資源池,它有五個字元串,分别是"apple"、“banana”、“orange”、“<b>man</b><i>go</i>”和“pear”。注意到第四個字元串“<b>man</b><i>go</i>”,它實際表示的是一個字元串“mango”,不過它的前三個字元“man”通過b标簽來描述為粗體的,而後兩個字元通過i标簽來描述為斜體的。這樣實際上在整個字元串資源池中,包含了七個字元串,分别是"apple"、“banana”、“orange”、“mango”、“pear”、“b”和“i”,其中,第四個字元串“mango”來有兩個sytle,第一個style表示第1到第3個字元是粗體的,第二個style表示第4到第5個字元是斜體的。

        字元串與其樣式描述是一一對應的,也就是說,如果第i個字元串是帶有樣式描述的,那麼它的樣式描述就位于樣式内容塊第i個位置上。以上面的字元串資源池為例,由于第4個字元中帶有樣式描述,為了保持字元串與樣式描述的一一對應關系,那麼也需要假設前面3個字元串也帶有樣式描述的,不過需要将這3個字元串的樣式描述的個數設定為0。也就是說,在這種情況下,字元串的個數等于7,而樣式描述的個數等于4,其中,第1到第3個字元串的樣式描述的個數等于0,而第4個字元串的樣式描述的個數等于2。

        假設一個字元串有N個樣式描述,那麼它在樣式内容塊中,就對應有N個ResStringPool_span,以及一個ResStringPool_ref,其中,N個ResStringPool_span位于前面,用來描述每一個樣式,而ResStringPool_ref表示一個結束占位符。例如,對于上述的“mango”字元串來說,它就對應有2個ResStringPool_span,以及1個ResStringPool_ref,而對于"apple"、“banana”和“orange”這三個字元串來說,它們對應有0個ResStringPool_span,但是對應有1個ResStringPool_ref,最後三個字元串“pear”、“b”和"i"對應有0個ResStringPool_span和0個ResStringPool_ref。

        ResStringPool_span和ResStringPool_ref定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * Reference to a string in a string pool.
 */
struct ResStringPool_ref
{
    // Index into the string pool table (uint32_t-offset from the indices
    // immediately after ResStringPool_header) at which to find the location
    // of the string data in the pool.
    uint32_t index;
};

/**
 * This structure defines a span of style information associated with
 * a string in the pool.
 */
struct ResStringPool_span
{
    enum {
        END = 0xFFFFFFFF
    };

    // This is the name of the span -- that is, the name of the XML
    // tag that defined it.  The special value END (0xFFFFFFFF) indicates
    // the end of an array of spans.
    ResStringPool_ref name;

    // The range of characters in the string that this span applies to.
    uint32_t firstChar, lastChar;
};
           

        由于ResStringPool_ref在這裡出現的作用就是充當樣式描述結束占位符,是以,它唯一的成員變量index的取值就固定為ResStringPool_span::END。

        再來看ResStringPool_span是如何表示一個樣式描述的。以字元串“mango”的第一個樣式描述為例,對應的ResStringPool_span的各個成員變量的取值為:

        --name:等于字元串“b”在字元串資源池中的位置。

        --firstChar:等于0,即指向字元“m”。

        --lastChar:等于2,即指向字元"n"。

        綜合起來就是表示字元串“man”是粗體的。

        再以字元串“mango”的第二個樣式描述為例,對應的ResStringPool_span的各個成員變量的取值為:

        --name:等于字元串“i”在字元串資源池中的位置。

        --firstChar:等于3,即指向字元“g”。

        --lastChar:等于4,即指向字元“o”。

        綜合起來就是表示字元串“go”是斜體的。

        另外有一個地方需要注意的是,字元串樣式内容的最後會有8個位元組,每4個位元組都被填充為ResStringPool_span::END,用來表達字元串樣式内容結束符。這個結束符可以在解析過程中用作錯誤驗證。

        Step 5. 寫入資源ID

        在前面的Step 1中,我們把屬性的資源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來表示,如圖14所示:

Android應用程式資源的編譯和打包過程分析

圖14 命名空間chunk塊

       ResXMLTree_node和ResXMLTree_namespaceExt定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * Basic XML tree node.  A single item in the XML document.  Extended info
 * about the node can be found after header.headerSize.
 */
struct ResXMLTree_node
{
    struct ResChunk_header header;

    // Line number in original source file at which this element appeared.
    uint32_t lineNumber;

    // Optional XML comment that was associated with this element; -1 if none.
    struct ResStringPool_ref comment;
};

/**
 * Extended XML tree node for namespace start/end nodes.
 * Appears header.headerSize bytes after a ResXMLTree_node.
 */
struct ResXMLTree_namespaceExt
{
    // The prefix of the namespace.
    struct ResStringPool_ref prefix;

    // The URI of the namespace.
    struct ResStringPool_ref uri;
};
           

        對于main.xml檔案來說,在它的命名空間chunk中,内嵌在第一個ResXMLTree_node裡面的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_XML_START_NAMESPACE_TYPE,表示命名空間開始标簽的頭部。

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

        --size:等于sizeof(ResXMLTree_node) + sizeof(ResXMLTree_namespaceExt)。

        第一個ResXMLTree_node的其餘成員變量的取值如下所示:

        --lineNumber:等于命名空間開始标簽在原來文本格式的Xml檔案出現的行号。

        --comment:等于命名空間的注釋在字元池資源池的索引。

       内嵌在第二個ResXMLTree_node裡面的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_XML_END_NAMESPACE_TYPE,表示命名空間結束标簽的頭部。

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

        --size:等于sizeof(ResXMLTree_node) + sizeof(ResXMLTree_namespaceExt)。

        第二個ResXMLTree_node的其餘成員變量的取值如下所示:

        --lineNumber:等于命名空間結束标簽在原來文本格式的Xml檔案出現的行号。

        --comment:等于0xffffffff,即-1。

        兩個ResXMLTree_namespaceExt的内容都是一樣的,它們的成員變量的取值如下所示:

        --prefix:等于字元串“android”在字元串資源池中的索引。

        --uri:等于字元串“http://schemas.android.com/apk/res/android”在字元串資源池中的索引。

        接下來被壓平的是标簽為LinearLayout的Xml Node。這個Xml Node由兩個ResXMLTree_node、一個ResXMLTree_attrExt、一個ResXMLTree_endElementExt和四個ResXMLTree_attribute來表示,如圖15所示:

Android應用程式資源的編譯和打包過程分析

圖15 标簽為LinearLayout的Xml元素chunk

        ResXMLTree_attrExt、ResXMLTree_attribute和ResXMLTree_endElementExt定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * Extended XML tree node for start tags -- includes attribute
 * information.
 * Appears header.headerSize bytes after a ResXMLTree_node.
 */
struct ResXMLTree_attrExt
{
    // String of the full namespace of this element.
    struct ResStringPool_ref ns;

    // String name of this node if it is an ELEMENT; the raw
    // character data if this is a CDATA node.
    struct ResStringPool_ref name;

    // Byte offset from the start of this structure where the attributes start.
    uint16_t attributeStart;

    // Size of the ResXMLTree_attribute structures that follow.
    uint16_t attributeSize;

    // Number of attributes associated with an ELEMENT.  These are
    // available as an array of ResXMLTree_attribute structures
    // immediately following this node.
    uint16_t attributeCount;

    // Index (1-based) of the "id" attribute. 0 if none.
    uint16_t idIndex;

    // Index (1-based) of the "class" attribute. 0 if none.
    uint16_t classIndex;

    // Index (1-based) of the "style" attribute. 0 if none.
    uint16_t styleIndex;
};

struct ResXMLTree_attribute
{
    // Namespace of this attribute.
    struct ResStringPool_ref ns;

    // Name of this attribute.
    struct ResStringPool_ref name;

    // The original raw string value of this attribute.
    struct ResStringPool_ref rawValue;

    // Processesd typed value of this attribute.
    struct Res_value typedValue;
};

/**
 * Extended XML tree node for element start/end nodes.
 * Appears header.headerSize bytes after a ResXMLTree_node.
 */
struct ResXMLTree_endElementExt
{
    // String of the full namespace of this element.
    struct ResStringPool_ref ns;

    // String name of this node if it is an ELEMENT; the raw
    // character data if this is a CDATA node.
    struct ResStringPool_ref name;
};
           

        内嵌在第一個ResXMLTree_node裡面的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_XML_START_ELEMENT_TYPE,表示LinearLayout開始标簽的頭部。

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

        --size:等于sizeof(ResXMLTree_node) + sizeof(ResXMLTree_attrExt) + sizeof(ResXMLTree_attribute) * 4。

        第一個ResXMLTree_node的其餘成員變量的取值如下所示:

        --lineNumber:等于LinearLayout開始标簽在原來文本格式的Xml檔案出現的行号。

        --comment:等于LinearLayout标簽的注釋在字元池資源池的索引。

        ResXMLTree_attrExt的各個成員變量的取值如下所示:

        --ns:等于LinearLayout元素的指令空間在字元池資源池的索引,沒有指定則等于-1。

        --name:等于字元串“LinearLayout”在字元池資源池的索引。

        --attributeStart:等于sizeof(ResXMLTree_attrExt),表示LinearLayout的屬性chunk相對type值為RES_XML_START_ELEMENT_TYPE的ResXMLTree_node頭部的位置。

        --attributeSize:等于sizeof(ResXMLTree_attribute),表示每一個屬性占據的chunk大小。

        --attributeCount:等于4,表示有4個屬性chunk。

        --idIndex:如果LinearLayout元素有一個名稱為“id”的屬性,那麼就将它出現在屬性清單中的位置再加上1的值記錄在idIndex中,否則的話,idIndex的值就等于0。

        --classIndex:如果LinearLayout元素有一個名稱為“class”的屬性,那麼就将它出現在屬性清單中的位置再加上1的值記錄在classIndex中,否則的話,classIndex的值就等于0。

        --styleIndex:如果LinearLayout元素有一個名稱為“style”的屬性,那麼就将它出現在屬性清單中的位置再加上1的值記錄在styleIndex中,否則的話,styleIndex的值就等于0。

        LinearLayout元素有四個屬性,每一個屬性都對應一個ResXMLTree_attribute,接下來我們就以名稱為“orientation”的屬性為例,來說明它的各個成員變量的取值,如下所示:

        --ns:等于屬性orientation的指令空間在字元池資源池的索引,沒有指定則等于-1。

        --name:等于屬性名稱字元串“orientation”在字元池資源池的索引。

        --rawValue:等于屬性orientation的原始值“vertical”在字元池資源池的索引,這是可選的,如果不用保留,它的值就等于-1。

        名稱為“orientation”的ResXMLTree_attribute的成員變量typedValue是一個Res_value,它定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * Representation of a value in a resource, supplying type
 * information.
 */
struct Res_value
{
    // Number of bytes in this structure.
    uint16_t size;

    // Always set to 0.
    uint8_t res0;

    // Type of the data value.
    enum {
        // Contains no data.
        TYPE_NULL = 0x00,
        // The 'data' holds a ResTable_ref, a reference to another resource
        // table entry.
        TYPE_REFERENCE = 0x01,
        // The 'data' holds an attribute resource identifier.
        TYPE_ATTRIBUTE = 0x02,
        // The 'data' holds an index into the containing resource table's
        // global value string pool.
        TYPE_STRING = 0x03,
        // The 'data' holds a single-precision floating point number.
        TYPE_FLOAT = 0x04,
        // The 'data' holds a complex number encoding a dimension value,
        // such as "100in".
        TYPE_DIMENSION = 0x05,
        // The 'data' holds a complex number encoding a fraction of a
        // container.
        TYPE_FRACTION = 0x06,

        // Beginning of integer flavors...
        TYPE_FIRST_INT = 0x10,

        // The 'data' is a raw integer value of the form n..n.
        TYPE_INT_DEC = 0x10,
        // The 'data' is a raw integer value of the form 0xn..n.
        TYPE_INT_HEX = 0x11,
        // The 'data' is either 0 or 1, for input "false" or "true" respectively.
        TYPE_INT_BOOLEAN = 0x12,

        // Beginning of color integer flavors...
        TYPE_FIRST_COLOR_INT = 0x1c,

        // The 'data' is a raw integer value of the form #aarrggbb.
        TYPE_INT_COLOR_ARGB8 = 0x1c,
        // The 'data' is a raw integer value of the form #rrggbb.

        // The 'data' is a raw integer value of the form #aarrggbb.
        TYPE_INT_COLOR_ARGB8 = 0x1c,
        // The 'data' is a raw integer value of the form #rrggbb.
        TYPE_INT_COLOR_RGB8 = 0x1d,
        // The 'data' is a raw integer value of the form #argb.
        TYPE_INT_COLOR_ARGB4 = 0x1e,
        // The 'data' is a raw integer value of the form #rgb.
        TYPE_INT_COLOR_RGB4 = 0x1f,

        // ...end of integer flavors.
        TYPE_LAST_COLOR_INT = 0x1f,

        // ...end of integer flavors.
        TYPE_LAST_INT = 0x1f
    };
    uint8_t dataType;

    // Structure of complex data values (TYPE_UNIT and TYPE_FRACTION)
    enum {
        // Where the unit type information is.  This gives us 16 possible
        // types, as defined below.
        COMPLEX_UNIT_SHIFT = 0,
        COMPLEX_UNIT_MASK = 0xf,

        // TYPE_DIMENSION: Value is raw pixels.
        COMPLEX_UNIT_PX = 0,
        // TYPE_DIMENSION: Value is Device Independent Pixels.
        COMPLEX_UNIT_DIP = 1,
        // TYPE_DIMENSION: Value is a Scaled device independent Pixels.
        COMPLEX_UNIT_SP = 2,
        // TYPE_DIMENSION: Value is in points.
        COMPLEX_UNIT_PT = 3,
        // TYPE_DIMENSION: Value is in inches.
        COMPLEX_UNIT_IN = 4,
        // TYPE_DIMENSION: Value is in millimeters.
        COMPLEX_UNIT_MM = 5,

        // TYPE_FRACTION: A basic fraction of the overall size.
        COMPLEX_UNIT_FRACTION = 0,
        // TYPE_FRACTION: A fraction of the parent size.
        COMPLEX_UNIT_FRACTION_PARENT = 1,

        // Where the radix information is, telling where the decimal place
        // appears in the mantissa.  This give us 4 possible fixed point
        // representations as defined below.
        COMPLEX_RADIX_SHIFT = 4,
        COMPLEX_RADIX_MASK = 0x3,

        // The mantissa is an integral number -- i.e., 0xnnnnnn.0
        COMPLEX_RADIX_23p0 = 0,
        // The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn
        COMPLEX_RADIX_16p7 = 1,
        // The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn
        COMPLEX_RADIX_8p15 = 2,

        // The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn
        COMPLEX_RADIX_0p23 = 3,

        // Where the actual value is.  This gives us 23 bits of
        // precision.  The top bit is the sign.
        COMPLEX_MANTISSA_SHIFT = 8,
        COMPLEX_MANTISSA_MASK = 0xffffff
    };

    // The data for this item, as interpreted according to dataType.
    uint32_t data;

    void copyFrom_dtoh(const Res_value& src);
};
           

         一個屬性的值經過解析之後,也就是經過前面編譯Xml資源的第3個操作之後,就用一個Res_value來表示。 例如,對于名稱為“orientation”的屬性的值“vertical”來說,經過解析之後,它就會用一個Res_value來表示,這個Res_value的各個成員變量的值如下所示:

        --size:等于sizeof(Res_value)。

        --res0:等于0,保留給以後用。

        --dataType:等于TYPE_INT_DEC,表示資料類型,即這是一個十進制形式的整數值。

        --data:等于1,參考前面編譯Xml資源的第3個操作。

        更多的資料類型,請參考Res_value定義裡面的注釋。從這裡我們就可以看出,在解析二進制格式的Xml檔案的過程中,當我們知道一個屬性的名稱在字元串資源池的索引之後,就可以通過這個索引在字元串資源池中找到對應的屬性名稱字元中,同時,通過這個索引還可以在資源ID的那塊chunk中找到對應的屬性資源ID,而有了這個屬性資源ID之後,我們就可以進一步地驗證該屬性的取值是否正确等操作。

        内嵌在第二個ResXMLTree_node裡面的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_XML_END_ELEMENT_TYPE,表示LinearLayout結束标簽的頭部。

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

        --size:等于sizeof(ResXMLTree_node) + sizeof(ResXMLTree_endElementExt) 。

        第二個ResXMLTree_node的其餘成員變量的取值如下所示:

        --lineNumber:等于LinearLayout結束标簽在原來文本格式的Xml檔案出現的行号。

        --comment:等于-1。

        ResXMLTree_endElementExt的各個成員變量的取值如下所示:

        --ns:等于LinearLayout元素的指令空間在字元池資源池的索引,沒有指定則等于-1。

        --name:等于字元串“LinearLayout”在字元池資源池的索引。

        注意,位于名稱為“gravity”的ResXMLTree_attribute和第二個ResXMLTree_node之間的chunk是用來寫入LinearLayout元素的兩個子元素Button的内容的。這兩個Button與LinearLayout一樣,都是以相同的結構遞歸寫入到最終的二進制Xml檔案中去的,差別隻在于結構内容的不同,以及屬性個數的不同。

        例如,對于第一個Button的屬性id來說,它所對應的ResXMLTree_attribute的成員變量typedValue所指向的一個Res_value的各個成員變量的值如下所示:

        --size:等于sizeof(Res_value)。

        --res0:等于0,保留給以後用。

        --dataType:等于TYPE_REFERENCE,表示資料類型,即這是一個引用值。

        --data:等于配置設定給名稱為“button_start_in_process”的ID的資源ID值,它的Package ID等于0x7f,而Type ID等于0x04,Entry ID取決于它的出現次序。

        又如,對于第一個Button的屬性text來說,它所對應的ResXMLTree_attribute的成員變量typedValue所指向的一個Res_value的各個成員變量的值如下所示:

        --size:等于sizeof(Res_value)。

        --res0:等于0,保留給以後用。

        --dataType:等于TYPE_REFERENCE,表示資料類型,即這是一個引用值。

        --data:等于配置設定給名稱“start_in_process”的字元串的資源ID值,它的Package ID等于0x7f,而Type ID等于0x05,Entry ID取決于它的出現次序。

        對于一個Xml檔案來說,它除了有命名空間和普通标簽類型的Node之外,還有一些稱為CDATA類型的Node,例如,假設一個Xml檔案,它的一個Item标簽的内容如下所示:

......

    <Item>This is a normal text</Item>

......
           

        那麼字元串“This is a normal text”就稱為一個CDATA,它在二進制Xml檔案中用一個ResXMLTree_node和一個ResXMLTree_cdataExt來描述,如圖16所示:

Android應用程式資源的編譯和打包過程分析

圖16 CDATA類型的Xml Node的二進制表示

         ResXMLTree_cdataExt定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * Extended XML tree node for CDATA tags -- includes the CDATA string.
 * Appears header.headerSize bytes after a ResXMLTree_node.
 */
struct ResXMLTree_cdataExt
{
    // The raw CDATA character data.
    struct ResStringPool_ref data;

    // The typed value of the character data if this is a CDATA node.
    struct Res_value typedData;
};
           

         内嵌在上面的ResXMLTree_node的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_XML_CDATA_TYPE,表示CDATA頭部。

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

        --size:等于sizeof(ResXMLTree_node) + sizeof(ResXMLTree_cdataExt) 。

        上面的ResXMLTree_node的其餘成員變量的取值如下所示:

        --lineNumber:等于字元串“This is a normal text”在原來文本格式的Xml檔案出現的行号。

        --comment:等于字元串“This is a normal text”的注釋,如果沒有則等于-1。

        下面的ResXMLTree_cdataExt的成員變量data等于字元串“This is a normal text”在字元串資源池的索引,另外一個成員變量typedData所指向的一個Res_value的各個成員變量的值如下所示:

        --size:等于sizeof(Res_value)。

        --res0:等于0,保留給以後用。

        --dataType:等于TYPE_NULL,表示沒有包含資料,資料已經包含在ResXMLTree_cdataExt的成員變量data中。

        --data:等于0,由于dataType等于TYPE_NULL,這個值是沒有意義的。

        至此,一個Xml檔案就從按照圖8以及圖10的步驟從文本格式編譯成二進制格式了。當所有的Xml檔案都編譯完成之後,接下來就開始生成資源符号了。

        八. 生成資源符号

        這裡生成資源符号為後面生成R.java檔案做好準備的。從前面的操作可以知道,所有收集到的資源項都按照類型來儲存在一個資源表中,即儲存在一個ResourceTable對象。是以,Android資源打包工具aapt隻要周遊每一個Package裡面的每一個Type,然後取出每一個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”是第三個出現的字元串。

        九. 生成資源索引表

        我們首先總結一下,經過上述八個操作之後,所獲得的資源清單如圖17所示:

Android應用程式資源的編譯和打包過程分析

圖17 收集到的所有資源項

        有了這些資源項之後,Android資源打包工具aapt就可以按照下面的流程來生成資源索引表resources.arsc了,如圖18所示:

Android應用程式資源的編譯和打包過程分析

圖18 資源索引表的生成過程

        接下來,我們就以圖17所示的資源項來說圖18所示的資源索引表生成過程。

        1. 收集類型字元串

        在圖17所示的資源項中,一共有4種類型的資源,分别是drawable、layout、string和id,于是對應的類型字元串就為“drawable”、“layout”、“string”和“id”。

        注意,這些字元串是按Package來收集的,也就是說,目前被編譯的應用程式資源有幾個Package,就有幾組對應的類型字元串,每一個組類型字元串都儲存在其所屬的Package中。

        2. 收集資源項名稱字元串

        在圖17所示的資源項中,一共有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”,于是收集到的資源項名稱字元串就為“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來收集的,也就是說,目前被編譯的應用程式資源有幾個Package,就有幾組對應的資源項名稱字元串,每一個組資源項名稱字元串都儲存在其所屬的Package中。

        3. 收集資源項值字元串

        在圖17所示的資源項中,一共有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來收集的,也就是說,目前所有參與編譯的Package的資源項值字元串都會被統一收集在一起。

        4. 生成Package資料塊

        參與編譯的每一個Package的資源項元資訊都寫在一塊獨立的資料上,這個資料塊使用一個類型為ResTable_package的頭部來描述。每一個Package的資源項元資訊資料塊的生成過程如圖19所示:

Android應用程式資源的編譯和打包過程分析

圖19 Package資源項元資訊資料塊的生成過程

        這個生成過程可以分為5個步驟,接下來我們就以圖17所示的資源項來說詳細分析每一個步驟。

        Step 1. 寫入Package資源項元資訊資料塊頭部

        Package資源項元資訊資料塊頭部是用一個ResTable_package來定義的。ResTable_package定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * 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.
    char16_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;
};
           

        嵌入在ResTable_package内部的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_TABLE_PACKAGE_TYPE,表示這是一個Package資源項元資訊資料塊頭部。

        --headerSize:等于sizeof(ResTable_package),表示頭部大小。

        --size:等于sizeof(ResTable_package) + 類型字元串資源池大小 + 資源項名稱字元串資源池大小 + 類型規範資料塊大小 + 資料項資訊資料塊大小。

        ResTable_package的其它成員變量的取值如下所示:

        --id:等于Package ID。

        --name:等于Package Name。

        --typeStrings:等于類型字元串資源池相對頭部的偏移位置。

        --lastPublicType:等于最後一個導出的Public類型字元串在類型字元串資源池中的索引,目前這個值設定為類型字元串資源池的大小。

        --keyStrings:等于資源項名稱字元串相對頭部的偏移位置。

        --lastPublicKey:等于最後一個導出的Public資源項名稱字元串在資源項名稱字元串資源池中的索引,目前這個值設定為資源項名稱字元串資源池的大小。

        我們可以通過圖20來清楚地看到一個Package資源項元資訊資料塊的結構:

Android應用程式資源的編譯和打包過程分析

圖20 Package資源項元資訊資料塊結構

        在Android資源中,有一種資源類型稱為Public,它們一般是定義在res/values/public.xml檔案中,形式如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <public type="string" name="string3" id="0x7f040001" />
</resources>
           

        這個public.xml用來告訴Android資源打包工具aapt,将類型為string的資源string3的ID固定為0x7f040001。為什麼需要将某一個資源項的ID固定下來呢?一般來說,當我們将自己定義的資源導出來給第三方應用程式使用時,為了保證以後修改這些導出資源時,仍然保證第三方應用程式的相容性,就需要給那些導出資源一個固定的資源ID。

        每當Android資源打包工具aapt重新編譯被修改過的資源時,都會重新給這些資源賦予ID,這就可能會造成同一個資源項在兩次不同的編譯中被賦予不同的ID。這種情況就會給第三方應用程式程式帶來麻煩,因為後者一般是假設一個ID對應的永遠是同一個資源的。是以,當我們将自己定義的資源導出來給第三方應用程式使用時,就需要通過public.xml檔案将導出來的資源的ID固定下來。

        我們用一個例子來說public.xml檔案的作用,考慮下面這個strings.xml檔案:

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

        假設Android資源打包工具aapt為字元串資源項string1和string3配置設定到的資源ID如下所示:

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

        這時候第三方應用程式就會認為0x7f040001引用的永遠是字元串“String 3”。

        假設将來的某一天,我們需要在strings.xml檔案中增加一個新的字元串,如下所示:

<?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>
           

        如果沒有上述的public.xml檔案,那麼Android資源打包工具aapt為字元串資源項string1、 string2和string3配置設定的資源ID就會如下所示:

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

        這就完蛋了,這時候第三方應用程式通過0x7f040001引用到的字元串變成了“String 2”。

        如果我們使用上述的public.xml檔案将字元串“String 3”固定為0x7f040001,那麼Android資源打包工具aapt為字元串資源項string1、 string2和string3配置設定的資源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
    }
}
           

        這樣第三方應用程式通過0x7f040001引用到的字元串仍然是“String 3”。

        注意,我們在開發應用程式時,一般是不需要用到public.xml檔案的,因為我們的資源基本上都是在内部使用的,不會導出來給第三方應用程式使用。隻在内部使用的資源,不管它的ID如何變化,我們都可以通過R.java檔案定義的常量來正确地引用它們。隻有系統定義的資源包才會使用到public.xml檔案,因為它定義的資源是需要提供給第三方應用程式使用的。

        Step 2. 寫入類型字元串資源池

        在前面的第1個操作中,我們已經将每一個Package用到的類型字元串收集起來了,是以,這裡就可以直接将它們寫入到Package資源項元資訊資料塊頭部後面的那個資料塊去。

        Step 3. 寫入資源項名稱字元串資源池

        在前面的第2個操作中,我們已經将每一個Package用到的資源項名稱字元串收集起來了,這裡就可以直接将它們寫入到類型字元串資源池後面的那個資料塊去。

        Step 4. 寫入類型規範資料塊

        類型規範資料塊用來描述資源項的配置差異性。通過這個差異性描述,我們就可以知道每一個資源項的配置狀況。知道了一個資源項的配置狀況之後,Android資源管理架構在檢測到裝置的配置資訊發生變化之後,就可以知道是否需要重新加載該資源項。類型規範資料塊是按照類型來組織的,也就是說,每一種類型都對應有一個類型規範資料塊。

        類型規範資料塊的頭部是用一個ResTable_typeSpec來定義的。ResTable_typeSpec定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * 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
 * configuation 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 {
        // Additional flag indicating an entry is public.
        SPEC_PUBLIC = 0x40000000
    };
};
           

        嵌入在ResTable_typeSpec裡面的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_TABLE_TYPE_SPEC_TYPE,用來描述一個類型規範頭部。

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

        --size:等于sizeof(ResTable_typeSpec) + sizeof(uint32_t) * entryCount,其中,entryCount表示本類型的資源項個數。

        ResTable_typeSpec的其它成員變量的取值如下所示:

        --id:表示資源的Type ID。

        --res0:等于0,保留以後使用。

        --res1:等于0,保留以後使用。

        --entryCount:等于本類型的資源項個數,注意,這裡是指名稱相同的資源項的個數。

        ResTable_typeSpec後面緊跟着的是一個大小為entryCount的uint32_t數組,每一個數組元數,即每一個uint32_t,都是用來描述一個資源項的配置差異性的。例如,在圖17中,名稱為icon的drawable資源項有三種不同的螢幕配置ldpi、mdpi和hdpi,于是用來描述它的配置差異性的uint32_t的第CONFIG_DENSITY位就等于1,而其餘位都等于0。又如,在圖17中,名稱為main的layout資源項隻有一種配置default,于是用來描述它的配置差異性的uint32_t的值就等于0。此外,如果一個資源項是導出的,即它的資源ID是通過public.xml來固定的,那麼用來描述它的配置差異性的uint32_t的第ResTable_typeSpec::SPEC_PUBLIC位也會被設定為1。

        在圖17中,一共有4種不同類型的資源項,它們所對應的4個類型規範資料塊如圖21至圖24所示:

Android應用程式資源的編譯和打包過程分析

圖21 類型為drawable的規範資料塊

Android應用程式資源的編譯和打包過程分析

圖22 類型為layout的規範資料塊

Android應用程式資源的編譯和打包過程分析

圖23 類型為string的規範資料塊

Android應用程式資源的編譯和打包過程分析

圖24 類型為id的規範資料塊

        從圖21到圖24就可以看出,類型為drawable的資源項icon在裝置的螢幕密度發生變化之後,Android資源管理架構需要重新對它進行加載,以便獲得更合适的資源項,而其它資源項無論裝置配置資訊發生何種變化,它們都不需要重新加載,因為它們隻有一種配置。

        Step 5. 寫入類型資源項資料塊

        類型資源項資料塊用來描述資源項的具體資訊, 這樣我們就可以知道每一個資源項名稱、值和配置等資訊。類型資源項資料同樣是按照類型和配置來組織的,也就是說,一個具有N個配置的類型一共對應有N個類型資源項資料塊。

        類型資源項資料塊的頭部是用一個ResTable_type來定義的。ResTable_type定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * A collection of resource entries for a particular resource data
 * type. Followed by an array of uint32_t defining the resource
 * values, corresponding to the array of type strings in the
 * ResTable_package::typeStrings string block. Each of these hold an
 * index from entriesStart; a value of NO_ENTRY means that entry is
 * not defined.
 *
 * There may be multiple of these chunks for a particular resource type,
 * supply different configuration variations for the resource values of
 * that type.
 *
 * It would be nice to have an additional ordered index of entries, so
 * we can do a binary search if trying to find a resource by string name.
 */
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;

    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;

    // 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.
    ResTable_config config;
};
           

        嵌入在ResTable_type裡面的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_TABLE_TYPE_TYPE,用來描述一個類型資源項頭部。

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

        --size:等于sizeof(ResTable_type) + sizeof(uint32_t) * entryCount,其中,entryCount表示本類型的資源項個數。

        ResTable_type的其它成員變量的取值如下所示:

        --id:表示資源的Type ID。

        --res0:等于0,保留以後使用。

        --res1:等于0,保留以後使用。

        --entryCount:等于本類型的資源項個數,注意,這裡是指名稱相同的資源項的個數。

        --entriesStart:等于資源項資料塊相對頭部的偏移值。

        --config:指向一個ResTable_config,用來描述配置資訊,它的定義可以參考圖2的類圖。

        ResTable_type緊跟着的是一個大小為entryCount的uint32_t數組,每一個數組元數,即每一個uint32_t,都是用來描述一個資源項資料塊的偏移位置。緊跟在這個uint32_t數組後面的是一個大小為entryCount的ResTable_entry數組,每一個數組元素,即每一個ResTable_entry,都是用來描述一個資源項的具體資訊。

        在圖17中,一共有4種不同類型的資源項,其中,類型為drawable的資源有1個資源項以及3種不同的配置,類型為layout的資源有2個資源項以及1種配置,類型為string的資源有5個資源項以及1種配置,類型為id的資源有2個資源項以及1種配置,這樣一共就對應有3 + 1 + 1 + 1個類型資源項資料塊,如圖25至圖圖30所示:

Android應用程式資源的編譯和打包過程分析

圖25 類型為drawable和配置為ldpi的資源項資料塊

Android應用程式資源的編譯和打包過程分析

圖26 類型為drawable和配置為mdpi的資源項資料塊

Android應用程式資源的編譯和打包過程分析

圖27 類型為drawable和配置為hdpi的資源項資料塊

Android應用程式資源的編譯和打包過程分析

圖28 類型為layout和配置為default的資源項資料塊

Android應用程式資源的編譯和打包過程分析

圖29 類型為string和配置為default的資源項資料塊

Android應用程式資源的編譯和打包過程分析

圖30 類型為id和配置為default的資源項資料塊

       注意,ResTable_type後面的uint32_t數組和ResTable_entry數組的大小不一定是相等的,考慮下面的資源目錄:

--res
  --drawable-ldpi
    --icon.png
  --drawable-mdpi
    --icon.png
    --logo.png
  --drawable-hdpi
    --logo.png
           

        那麼最終得到類型為drawable和配置為ldpi的資源項資料塊如圖31所示:

Android應用程式資源的編譯和打包過程分析

圖31 大小不等的uint32_t數組和ResTable_entry數組的資源項資料塊

        由于不存在類型為drawable、配置為ldpi,并且名稱為logo的資源項,是以,在圖31中,ResTable_type後面的uint32_t數組和ResTable_entry數組的大小是不相等的,并且沒有相應的ResTable_entry的uint32_t數組元素的值會被設定為ResTable_type::NO_ENTRY。

        每一個資源項資料都是通過一個 ResTable_entry來定義的。ResTable_entry定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * This is the beginning of information about an entry in the resource
 * table.  It holds the reference to the name of this entry, and is
 * immediately followed by one of:
 *   * A Res_value structure, if FLAG_COMPLEX is -not- set.
 *   * An array of ResTable_map structures, if FLAG_COMPLEX is set.
 *     These supply a set of name/value mappings of data.
 */
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;
};
           

        ResTable_entry的各個成員變量的取值如下所示:

        --size:等于sizeof(ResTable_entry),表示資源項頭部大小。

        --flags:資源項标志位。如果是一個Bag資源項,那麼FLAG_COMPLEX位就等于1,并且在ResTable_entry後面跟有一個ResTable_map數組,否則的話,在ResTable_entry後面跟的是一個Res_value。如果是一個可以被引用的資源項,那麼FLAG_PUBLIC位就等于1。

        --key:資源項名稱在資源項名稱字元串資源池的索引。

        接下來我們就分兩種情況來讨論資源項資訊寫入到資源索引表的過程。

        首先看一個普通的資源項,即一個非Bag資源項的寫入過程。從圖2可以知道,每一個資源項的資料都是用一個Item來描述的。在這個Item中,有一個類型為Res_value的成員變量parsedValue,它表示一個資源項經過解析後得到值。

        前面在分析Xml資源檔案的編譯過程時,我們已經介紹過Res_value的定義了。假設目前要寫入的資源項是類型為layout的main,從圖17可以知道,它的值是一個字元串“res/layout/main.xml”。字元串“res/layout/main.xml”在前面的第3步中已經被寫入到一個資源項值字元串池中去了,我們假設它被寫入在第3個位置上,那麼用來描述資源項main的Res_value的各個成員變量的取值如下所示:

        --size:等于sizeof(Res_value)。

        --res0:等于0,保留以後使用。

        --dataType:等于TYPE_STRING。

        --data:等于0x3。

        我們通過圖32來總結一個普通資源項寫入到資源表索引表的資料塊結構:

Android應用程式資源的編譯和打包過程分析

圖32 普通資源項寫入到資源索引表的資料塊結構

        接着看一個Bag資源項的寫入過程。以圖7所示的Bag資源項custom_orientation為例,它有本個bag,分别是^type、custom_vertical和custom_horizontal,其中,custom_vertical和custom_horizontal是兩個自定義的bag,它們的值分别等于0x0和0x1,而^type是一個系統内部定義的bag,它的值固定為0x10000。 注意,^type、custom_vertical和custom_horizontal均是類型為id的資源,假設它們配置設定的資源ID分别為0x1000000、0x7f040000和7f040001。

        一個Bag資源項寫入到資源索引表的資料塊結構如圖33所示:

Android應用程式資源的編譯和打包過程分析

圖33 Bag資源項寫入到資源索引表的資料塊結構

        在圖33中,緊跟在ResTable_entry後面的是一個ResTable_map_entry,用來描述後面要寫入到的ResTable_map的資訊。假設一個Bag資源項有N個bag,那麼在ResTable_map_entry就有N個ResTable_map。

        ResTable_map_entry定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 *  This is a reference to a unique entry (a ResTable_entry structure)
 *  in a resource table.  The value is structured as: 0xpptteeee,
 *  where pp is the package index, tt is the type index in that
 *  package, and eeee is the entry index in that type.  The package
 *  and type values start at 1 for the first item, to help catch cases
 *  where they have not been supplied.
 */
struct ResTable_ref
{
    uint32_t ident;
};

/**
 * Extended form of a ResTable_entry for map entries, defining a parent map
 * resource from which to inherit values.
 */
struct ResTable_map_entry : public ResTable_entry
{
    // Resource identifier of the parent mapping, or 0 if there is none.
    ResTable_ref parent;
    // Number of name/value pairs that follow for FLAG_COMPLEX.
    uint32_t count;
};
           

       ResTable_map_entry是從ResTable_entry繼承下來的,我們首先看ResTable_entry的各個成員變量的取值:

       --size:等于sizeof(ResTable_map_entry)。

       --flags:由于在緊跟在ResTable_map_entry前面的ResTable_entry的成員變量flags已經描述過資源項的标志位了,是以,這裡的flags就不用再設定了,它的值等于0。

       --key:由于在緊跟在ResTable_map_entry前面的ResTable_entry的成員變量key已經描述過資源項的名稱了,是以,這裡的key就不用再設定了,它的值等于0。

       ResTable_map_entry的各個成員變量的取值如下所示:

       --parent:指向父ResTable_map_entry的資源ID,如果沒有父ResTable_map_entry,則等于0。

       --count:等于bag項的個數。

       Bag資源項的每一個bag都用一個ResTable_map來表示。ResTable_map定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

struct ResTable_map
{
    // The resource identifier defining this mapping's name.  For attribute
    // resources, 'name' can be one of the following special resource types
    // to supply meta-data about the attribute; for all other resource types
    // it must be an attribute resource.
    ResTable_ref name;

    .....

    // This mapping's value.
    Res_value value;
};
           

        ResTable_map隻有兩個成員變量,其中:

        --name:等于bag的資源項ID。

        --value:等于bag的資源項值。

        例如,對于custom_vertical來說,用來描述它的ResTable_map的成員變量name的值就等于0x7f040000,而成員變量value所指向的一個Res_value的各個成員變量的值如下所示:

        --size:等于sizeof(Res_value)。

        --res0:等于0,保留以後使用。

        --dataType:等于TYPE_INT_DEC,表示data是一個十進制的整數。

        --data:等于0。

        我們可以依此類推,分别得到用來描述^type和custom_horizontal這兩個bag的ResTable_map的值。

        5. 寫入資源索引表頭部

        資源索引表頭部使用一個ResTable_header來表示。ResTable_header定義在檔案frameworks/base/include/utils/ResourceTypes.h中,如下所示:

/**
 * Header for a resource table.  Its data contains a series of
 * additional chunks:
 *   * A ResStringPool_header containing all table values.
 *   * 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;
};
           

        嵌入在ResTable_header内部的ResChunk_header的各個成員變量的取值如下所示:

        --type:等于RES_TABLE_TYPE,表示這是一個資源索引表頭部。

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

        --size:等于整個resources.arsc檔案的大小。

        ResTable_header的其它成員變量的取值如下所示:

        --packageCount:等于被編譯的資源包的個數。

        6. 寫入資源項的值字元串資源池

        在前面的第3步中,我們已經将所有的資源項的值字元串都收集起來了,是以,這裡直接它們寫入到資源索引表去就可以了。注意,這個字元串資源池包含了在所有的資源包裡面所定義的資源項的值字元串,并且是緊跟在資源索引表頭部的後面。

        7. 寫入Package資料塊

        在前面的第4步中,我們已經所有的Package資料塊都收集起來了,是以,這裡直接将它們寫入到資源索引表去就可以了。這些Package資料塊是依次寫入到資源索引表去的,并且是緊跟在資源項的值字元串資源池的後面。

        至此,資源項索引表的生成好了。

        十. 編譯AndroidManifest.xml檔案

        經過前面的九個步驟之後,應用程式的所有資源項就編譯完成了,這時候就開始将應用程式的配置檔案AndroidManifest.xml也編譯成二進制格式的Xml檔案。之是以要在應用程式的所有資源項都編譯完成之後,再編譯應用程式的配置檔案,是因為後者可能會引用到前者。

        應用程式配置檔案AndroidManifest.xml的編譯過程與其它的Xml資源檔案的編譯過程是一樣的,可以參考前面的第七步。注意,應用程式配置檔案AndroidManifest.xml編譯完成之後,Android資源打包工具appt還會驗證它的完整性和正确性,例如,驗證AndroidManifest.xml的根節點mainfest必須定義有android:package屬性。

       十一. 生成R.java檔案

       在前面的第八步中,我們已經将所有的資源項及其所對應的資源ID都收集起來了,是以,這裡隻要将直接将它們寫入到指定的R.java檔案去就可以了。例如,假設配置設定給類型為layout的資源項main和sub的ID為0x7f030000和0x7f030001,那麼在R.java檔案,就會分别有兩個以main和sub為名稱的常量,如下所示:

public final class R {
    ......

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

    ......
}
           

        注意,每一個資源類型在R.java檔案中,都有一個對應的内部類,例如,類型為layout的資源項在R.java檔案中對應的内部類為layout,而類型為string的資源項在R.java檔案中對應的内部類就為string。

        十二. 打包APK檔案

        所有資源檔案都編譯以及生成完成之後,就可以将它們打包到APK檔案去了,包括:

        1. assets目錄。

        2. res目錄,但是不包括res/values目錄, 這是因為res/values目錄下的資源檔案的内容經過編譯之後,都直接寫入到資源項索引表去了。

        3. 資源項索引檔案resources.arsc。

        當然,除了這些資源檔案外,應用程式的配置檔案AndroidManifest.xml以及應用程式代碼檔案classes.dex,還有用來描述應用程式的簽名資訊的檔案,也會一并被打包到APK檔案中去,這個APK檔案可以直接拿到模拟器或者裝置上去安裝。

        至此,我們就分析完成Android應用程式資源的編譯和打包過程了,其中最重要的是要掌握以下四個要點:

        1.  Xml資源檔案從文本格式編譯為二進制格式的過程。

        2. Xml資源檔案的二進制格式。

        3. 資源項索引表resources.arsc的生成過程。

        4. 資源項索引表resources.arsc的二進制格式。

        了解了Android應用程式資源的編譯和打包過程之後,接下來我們就可以分析Android應用程式在運作時查找索引的過程了,敬請關注!

老羅的新浪微網誌:http://weibo.com/shengyangluo,歡迎關注!

繼續閱讀