我們知道,在一個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資源打包工具的執行過程
假設我們當前要編譯的應用程序資源目錄結構如下所示:
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所示:
圖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所示:
圖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所示:
圖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所示:
圖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所示:
圖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所示:
圖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所示:
圖11 屬性名稱字符串與屬性資源ID的對應關係
Step 2. 收集其它字符串
這一步收集的是Xml文件中的其它所有字符串。由於在前面的Step 1中,那些具有資源ID的Xml元素屬性的名稱字符串已經被收集過了,因此,它們在一步中不會被重複收集。對於main.xml文件來說,這一步收集到的字符串如圖12所示:
圖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所示:
圖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所示:
圖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所示:
圖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所示:
圖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所示:
圖17 收集到的所有資源項
有了這些資源項之後,Android資源打包工具aapt就可以按照下面的流程來生成資源索引表resources.arsc了,如圖18所示:
圖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所示:
圖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資源項元信息數據塊的結構:
圖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所示:
圖21 類型爲drawable的規範數據塊
圖22 類型爲layout的規範數據塊
圖23 類型爲string的規範數據塊
圖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所示:
圖25 類型爲drawable和配置爲ldpi的資源項數據塊
圖26 類型爲drawable和配置爲mdpi的資源項數據塊
圖27 類型爲drawable和配置爲hdpi的資源項數據塊
圖28 類型爲layout和配置爲default的資源項數據塊
圖29 類型爲string和配置爲default的資源項數據塊
圖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所示:
圖31 大小不等的uint32_t數組和ResTable_entry數組的資源項數據塊
由於不存在類型爲drawable、配置爲ldpi,並且名稱爲logo的資源項,因此,在圖31中,ResTable_type後面的uint32_t數組和ResTable_entry數組的大小是不相等的,並且沒有相應的ResTable_entry的uint32_t數組元素的值會被設置爲ResTable_type::NO_ENTRY。
/** * 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來總結一個普通資源項寫入到資源表索引表的數據塊結構:
圖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所示:
圖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,歡迎關注!