1. 程式人生 > >Android應用程式資源的編譯和打包過程分析

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

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

        在前面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所示:


圖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所示:


圖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所示:


圖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所示:


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

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

  1. project  
  2.   --AndroidManifest.xml  
  3.   --res  
  4.     --drawable-ldpi  
  5.       --icon.png  
  6.     --drawable-mdpi  
  7.       --icon.png  
  8.     --drawable-hdpi  
  9.       --icon.png  
  10.     --layout  
  11.       --main.xml  
  12.       --sub.xml  
  13.     --values  
  14.       --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中,如下所示:

  1. class AaptAssets : public AaptDir  
  2. {  
  3.     ......  
  4. private:  
  5.     ......  
  6.     KeyedVector<String8, sp<ResourceTypeSet> >* mRes;  
  7. };  

        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,如下所示:

  1. class ResourceTable : public ResTable::Accessor  
  2. {  
  3.     ......  
  4. private:  
  5.     ......  
  6.     DefaultKeyedVector<String16, sp<Package> > mPackages;  
  7.     Vector<sp<Package> > mOrderedPackages;  
  8.     ......  
  9. };  

       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所示:


圖5 收集到的drawable和layout資源項列表

        五. 編譯values類資源

        型別為values的資源描述的都是一些簡單的值,如陣列、顏色、尺寸、字串和樣式值等,這些資源是在編譯的過程中進行收集的。接下來,我們就以字串的編譯過程來進行說明。

        在這篇文章中要用到的例子中,包含有一個strings.xml的檔案,它的內容如下所示:

  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <resources>
  3.     <stringname="app_name">Activity</string>
  4.     <stringname="sub_activity">Sub Activity</string>
  5.     <stringname="start_in_process">Start sub-activity in process</string>
  6.     <stringname="start_in_new_process">Start sub-activity in new process</string>
  7.     <stringname="finish">Finish activity</string>
  8. </resources>

        這個檔案經過編譯之後,資源表就多了一個名稱為string的Type,這個Type有五個ConfigList。這五個ConfigList的名稱分別為“app_name”、“sub_activity”、“start_in_process”、“start_in_new_process”和“finish”,每一個ConfigList又分別含有一個Entry。

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


圖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檔案,它的內容如下所示:

  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <resources>
  3.     <attrname="custom_orientation">
  4.         <enumname="custom_vertical"value="0"/>
  5.         <enumname="custom_horizontal"value="1"<