Android提供了一種非常靈活的資源系統,可以根據不同的條件提供可替代資源。因此,系統基于很少的改造就能支持新特性,比如Android N中的分屏模式。這也是Android強大部分之一。本文主要講述Android資源系統的實現原理,以及在應用開發中需要注意的事項。
定義資源
Android使用XML文件描述各種資源,包括字符串、顏色、尺寸、主題、布局、甚至是圖片(selector,layer-list)。
資源可分為兩部分,一部分是屬性,另一部分是值。對于android:text="hello,world"
,text
就是屬性,hello,world
就是值。
屬性的定義
在APK程序中,屬性定義在res/values/attrs.xml
中,在系統中屬性位于framework/base/core/res/res/values/attrs.xml
文件中。具體定義如下所示:
<declare-styleable name="Window">
<attr name="windowBackground" format="reference"/>
<attr name="windowContentOverlaly" />
<attr name="windowFrame" />
<attr name="windowTitle" />
</declare-styleable>
styleable相當于一個屬性集合,其在R.java文件中對應一個int[]數組,aapt為styleable中的每個attr(屬性)分配一個id值,int[]中的每個id對應著styleable中的每一個attr。
對于<declare-styleable name="Window">
,Window相當于屬性集合的名稱。
對于<attr name="windowBackground">
,windowBackground相當于屬性的名稱;屬性名稱在應用程序范圍內必須唯一,既無論定義幾個資源文件,無論定義幾個styleable,windowBackground必須唯一。
在Java代碼中,變量在一個作用域內只能聲明一次,但可以多次使用。attr
也是一樣,只能聲明一次,但可以多處引用。如上代碼所示,在Window中聲明了一個名為windowBackground的attr
,在Window中引用了一個名為windowTitle的attr
。
如果一個attr
后面僅僅有一個name
,那么這就是引用;如果不光有name
還有format
那就是聲明。windowBackground是屬性的聲明,其不能在其他styleable中再次聲明;windowTitle則是屬性的引用,其聲明是在別的styleable中。
值的定義
常見的值一般有以下幾種:
- String,Color,boolean,int類型:在
res/values/xxx.xml
文件中指定 - Drawable類型:在
res/drawable/xxx
中指定 - layout(布局):在
res/layout/xxx.xml
中指定 - style(樣式):在
res/values/xxx.xml
中指定
值的類型大致分為兩類,一類是基本類型,一類是引用類型;對于int,boolean等類型在聲明屬性時使用如下方式:
<attr name="width" format="integer"/>
<attr name="text" format="string" />
<attr name="centerInParent"="boolean"/>
對于Drawable,layout等類型在聲明屬性時:
<attr name="background" format="reference"/>
解析資源
資源解析主要涉及到兩個類,一個是AttributeSet,另一個是TypedArray。
AttributeSet
該類位于android.util.AttributeSet,純粹是一個輔助類,當從XML文件解析時會返回AttributeSet對象,該對象包含了解析元素的所有屬性及屬性值。并且在解析的屬性名稱與attrs.xml中定義的屬性名稱之間建立聯系。AttributeSet還提供了一組API接口從而可以方便的根據attrs.xml中已有的名稱獲取相應的值。
如果使用一般的XML解析工具,則可以通過類似getElementById()等方法獲取屬性的名稱和屬性值,然而這樣并沒有在獲取的屬性名稱與attrs.xml定義的屬性名稱之間建立聯系。
Attribute對象一般作為View的構造函數的參數傳遞過來,例如:
publlic TextView(Context context,AttributeSet attrs,int defStyle)
AttributeSet中的API可按功能分為以下幾類,假定TextView定義如下所示:
<TextView
android:id="@+id/tv"
android:layout_width="@dimen/width"
android:layout_height="wrap_content"
style="@stylel/text"
/>
第一類,操作特定屬性:
-
public String getIdAttribute()
,獲取id屬性對應的字符串,此處返回"@+id/tv" -
public String getStyleAttribute()
,獲取style屬性對應的字符串,返回"@style/text" -
public int getIdAttributeResourceValue(int defaultValue)
,返回id屬性對應的int值,此處對應R.id.tv。
第二類,操作通用屬性:
-
public int getAttributeCount()
,獲取屬性的數目,本例中返回4 -
public String getAttributeName(int index)
,根據屬性所在位置返回相應的屬性名稱。例如,id=0,layout_width=1,layout_height=2,style=3,如果getAttributeName(2),則返回android:layout_height -
public String getAttributeValue(int index)
,根據位置返回值。本例中,getAttributeValue(2)則返回"wrap_content"。 -
public String getAttributeValue(String namespace,String name)
,返回指定命名空間,指定名稱的屬性值,該方法說明AttributeSet允許給一個XML Element的屬性增加多個命名空間的屬性值。 -
public int getAttributeResource(int index)
,返回指定位置的屬性id值。本例中,getAttributeResource(2)返回R.attr.layout_width。前面也說過,系統會為每一個attr分配一個唯一的id。
第三類,獲取特定類型的值:
-
public XXXType getAttributeXXXType(int index,XXXType defaultValue)
,其中XXXType包括int、unsigned int、boolean、float類型。使用該方法時,必須明確知道某個位置(index)對應的數據類型,否則會返回錯誤。而且該方法僅適用于特定的類型,如果某個屬性值為一個style類型,或者為一個layout類型,那么返回值都將無效。
TypedArray
程序員在開發應用程序時,在XML文件中引用某個變量通常是android:background="@drawable/background",該引用對應的元素一般為某個View/ViewGroup,而View/ViewGroup的構造函數中會通過obatinStyledAttributes方法返回一個TypedArray對象,然后再調用對象中的getDrawable()方法獲取背景圖片。
TypedArray是對AttributeSet數據類的某種抽象。對于andorid:layout_width="@dimen/width"
,如果使用AttributeSet的方法,僅僅能獲取"@dimen/width"字符串。而實際上該字符串對應了一個dimen類型的數據。TypedArray可以將某個AttributeSet作為參數構造TypedArray對象,并提供更方便的方法直接獲取該dimen的值。
TypedArray a = context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.XXX,defStyle,0);
方法obtainStyledAttributes()的第一個參數是一個AttributeSet對象,它包含了一個XML元素中定義的所有屬性。第二個參數是前面定義的styleable,appt會把一個styleable編譯成一個int[]數組,該數組的內部實現正是通過遍歷AttributeSet中的每一個屬性,找到用戶感興趣的屬性,然后把值和屬性經過重定位,返回一個TypedArray對象。想要獲取某個屬性的值則調用相關的方法即可,比如TypedArray.getDrawbale(),TypedArray.getString()等。getDrawable(),getString()方法內部均通過Resources獲取屬性值。
加載資源
在使用資源時首先要把資源加載到內存。Resources的作用主要就是加載資源,應用程序需要的所有資源(包括系統資源)都是通過此對象獲取。一般情況下每個應用都會僅有一個Resources對象。
要訪問資源首先要獲取Resources對象。獲取Resources對象有兩種方法,一種是通過Context,一種是通過PackageManager。
使用Context獲取Resources
抽象類Context內部個有getResources()方法,一般是在Activity對象或者Service對象中調用,因為Activity或者Service的本質是一個Context,而真正實現Context接口的是ContextImpl類。
ContextImpl對象是在ActivityThread類中創建,所以getResources()方法實際上是調用ContextImpl.getResources()方法。在ContextImpl類中,該方法僅僅是返回內部的mResources變量,而對該變量賦值是在init()方法中。在創建ContextImpl對象后,一般會調用init()方法對ContextImpl對象內部變量初始化,其中就包括mResources變量,如以下代碼所示:
final void init(ActivityThread.PackageInfo packageInfo, IBinder activityToken, ActivityThread mainThread, Resources container){
mPackageInfo = packageInfo;
mResources = mPackageInfo.getResources(mainThread);
}
從以上代碼可以看出,mResources又是調用mPackageInfo的getResources()方法進行賦值。一個應用程序中可以有多個ContextImpl,但多個ContextImpl對象共享一個PackageInfo對象。所以多個ContextImpl對象中的mResources變量實際上是同一個Resources對象。
PackageInfo.getResources()方法如下所示:
public Resources getResources(ActivityThread mainThread){
if(mResources == null){
mResources = mainThread.getTopLevelResources(mResDir,this);
}
}
以上代碼中,參數mainThread指的就是ActivityThread對象,每個應用程序只有一個ActivityThread對象。getTopLevelResources()方法就是獲取本應用程序中的Resources對象。
在ActivityThread對象中,使用HashMap<ResourcesKey,WeakReference<Resources>> mActiveResources
保存該應用程序所有的Resources對象,并且這些Resources都是以一個弱引用保存起來的,這樣在內存緊張時可以釋放Resources所占的內存。
在mActiveResources中,使用ResourcesKey映射Resources類,ResourcesKey僅僅是一個數據類,其創建方式如下所示:
ResourcesKey key = new ResourcesKey(resDir,compInfo.applicatioScale);
resDir變量代表資源文件所在路徑,實際是指APK程序所在路徑,例如 /data/app/xxx.apk
。該APK會對應/data/dalvik-cache目錄下的data@app@xxx.apk@classes.dex文件,這兩個文件也是應用程序安裝后自動生成的文件。
如果一個應用程序沒有訪問該應用程序以外的資源,那么mActivieResources變量中就僅有一個Resources對象。當應用程序想要訪問其他應用程序的資源則需要構建不同的ResourcesKey,也就是需要不同的resDir,畢竟每一個ResourcesKey對應一個Resources對象,這樣該應用程序就可以訪問其他應用程序中的資源。
如果mActiveResources中還沒有包含所要的Resources對象,那就需要重新創建一個:
AssetManager assets = new AssetManager();
if(assets.addAssetPath(resDir) == 0){
return null;
}
DisplayMetrics metrics = getDisplayMetricsLocked(false);
r = new Resources(assets,metrics,getConfiguration(),compInfo);
創建Resources需要一個AssetManager對象。在開發應用程序時,使用Resources.getAssets()獲取的就是這里創建的AssetManager對象。AssetManager其實并不只是訪問res/assets目錄下的資源,而是可以訪問res目錄下的所有資源。
AssetManager在初始化的時候會被賦予兩個路徑,一個是應用程序資源路徑 /data/app/xxx.apk
,一個是Framework資源路徑/system/framework/framework-res.apk
(系統資源會被打包到此apk中)。所以應用程序使用本地Resources既可訪問應用程序資源,又可訪問系統資源。
AssetManager中很多獲取資源的關鍵方法都是native實現,當使用getXXX(int id)訪問資源時,如果id小于0x1000 0000時表示訪問系統資源,如果id都大于0x7000 0000則表示應用資源。aapt在對系統資源進行編譯時,所有資源id都被編譯為小于0x1000 0000。
當創建好Resources后就把該對象放到mActivieResources中以便以后繼續使用。
使用PackageManager獲取Resources
該方法主要是用來訪問其他應用程序中的資源,最典型的就是切換主題,但這種主題一般僅限于一個應用程序內部。獲取Resources的過程如下所示:
使用PackageManager獲取Resources對象:
PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.android...your package name");
其中getPackageManager()返回一個PackageManager對象,PackageManager本身是一個abstract類,其真正實現類是ApplicationPackageManager。其內部方法一般調用遠程PackageManagerService。ApplicationPackageManager在構造時傳入一個遠程服務的引用IPackageManager,該對象是通過調用getPackageManager()靜態方法獲取的。這種獲取遠程服務的方法和大多數獲取遠程服務的方法類似:
public static IPackageManager getPackageManager(){
if(sPackageManager !=null){
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}
獲得了PackageManager對象后,接著調用getResourcesForApplication()方法,該方法位于ContextImpl.ApplicationPackageManager中:
@Override
public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException{
if(app.packageName.equals("system")){
return mContext.mMainThread.getSystemContext().getResources();
}
Resources r = mContext.mMainThread.getTopLevelResources(app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir,mContext.mPackageInfo);
if(r != null){
return r;
}
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}
以上代碼內部調用mMainThread.getTopLevelResources()方法,又回到了使用Context獲取Resources對象的過程中。注意,此處調用參數的含義:如果目標資源程序和當前程序是同一個uid,那么就使用目標程序的sourceDir作為路徑,否則就使用目標程序的publicSourceDir目錄,該目錄可以在AndroidManifest.xml中指定。在大多數情況下,目標程序和當前程序不屬于同一個uid,因此,多為publicSourceDir,而該值默認情況下和sourceDir的值相同。
當進入mMainThread.getTopLevelResources()方法后,ActivityThread對象就會在mActivieResources變量中保存一個新的Resources對象,其鍵值對應目標程序的包名。
加載應用程序資源
應用程序打包的最終文件是xxx.apk。APK本身是一個zip文件,可以使用壓縮工具解壓。系統在安裝應用程序時首先解壓,并將其中的文件放到指定目錄。其中有一個文件名為resources.arsc,APK所有的資源均在其中定義。
resources.arsc是一種二進制格式的文件。aapt在對資源文件進行編譯時,會為每一個資源分配唯一的id值,程序在執行時會根據這些id值讀取特定的資源,而resources.arsc文件正是包含了所有id值得一個數據集合。在該文件中,如果某個id對應的資源是String或者數值(包括int,long等),那么該文件會直接包含相應的值,如果id對應的資源是某個layout或者drawable資源,那么該文件會存入對應資源的路徑地址。
事實上,當程序運行時,所需要的資源都要從原始文件中讀?。ˋPK在安裝時都會被系統拷貝到/data/app
目錄下)。加載資源時,首先加載resources.arsc,然后根據id值找到指定的資源。
加載Framework資源
系統資源是在zygote進程啟動時被加載的,并且只有當加載了系統資源后才開始啟動其他應用進程,從而實現其他應用進程共享系統資源的目標。
啟動第一步就是加載系統資源,加載完畢后再調用startSystemServer()啟動系統進程,并最后調用runSelectLoopMode()開始監聽Socket,并啟動指定的應用進程。加載系統資源是通過preLoadResources()完成的,該方法關鍵代碼如下所示:
mResources = Resources.getSystem();
mResources.startPreLoading();
if(PRELOAD_RESOURCES){
long startTime = SystemClock.uptimeMillis();
TypeArray ar = mResources.obtainTypedArray(com.android.internal.R.array.preloadingdrawables);
int N = prelaodDrawables(runtime,ar);
Log.i(TAG,"...preloading " + N + "resources in " + (SystemClock.uptimeMillis()-startTime) + "ms.");
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(com.android.internal.R.array.preloading_color_state_lists);
N = preloadingColorStateLists(runtime,ar);
Log.i(TAG,"...preloaded " + N + "resources in " + (SystemClock.uptimeMillis()-startTime) + "ms.");
}
mResources.finishPreloading();
在以上代碼中使用Resources.getSystem()創建Resources對象,一般情況下應用程序不應該調用此方法,因為該方法返回的Resources僅能訪問Framework資源。
當Resources對象創建完成后,調用preloadDrawables()和preloadColorStateLists()裝在需要"預裝載"的資源。這兩個方法都需要傳入一個TypeArray,其來源是res/values/arrays.xml中定義的一個array數組資源,例如:
<array name="preloaded_drawables">
<item>@drawable/sym_def_app_icon</item>
<item>@drawable/arrow_down_float</item>
</array>
<array name="preloaded_color_state_lists">
<item>@color/hint_foreground_dark</item>
<item>@color/hint_foreground_light</item>
</array>
在Resources類中,相關資源讀取函數需要將讀取到的資源緩沖起來,以便以后使用,Resources類中定義了四個靜態變量緩沖這些資源:
private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists = new LongSparseArray<ColorStateList>();
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<Drawable.ConstantState>();
private static boolean mPreloaded;
其中前三個變量是列表類型,并且被static修飾,所有Resources對象均共享這三個變量。所以當應用程序創建新的Resources對象時可以訪問系統資源。
第四個變量用來區分是zygote裝在資源還是普通應用進程裝在資源。因為zygote與普通進程裝載資源的方式類似,所以增加mPreloaded變量進行區分。
mPreloaded在startPreloading()中被置為true,在finishPreloading()中被置為false,而startPreloading()和finishPreloading()正是在ZygoteInit.java的preloadResources()中被調用,這就區別了zygote調用和普通進程調用。
最后,在Resources的具體資源讀取方法中,會判斷mPreloaded變量,如果為true,則同時把讀取到的資源存儲到三個靜態列表中,否則把資源放到非靜態列表中,這些非靜態列表的作用范圍為調用者所在進程。
Resources.loadDrawable()方法代碼如下所示:
if(mPreloading){
if(isColorDrawable){
sPreloadedColorDrawables.put(key,cs);
} else {
sPreloadedDrawables.put(key,cs);
}
} else {
synchronized(mTmpValue){
if(isColorDrawbale){
mColorDrawableCache.put(key,new WeakReference<ColorDrawable>(cs));
} else {
mDrawableCache.put(key,new WeakReference<Drawable>(cs));
}
}
}
上面所介紹的資源加載僅僅只是加載在res/values/arrays.xml中預先定義的資源值,Framework包含了更多的資源,zygote所加載的僅僅是一小部分。對于那些非"預裝載"的系統資源則不會被緩沖到靜態列表變量中,這時應用進程如果需要一個非預裝載資源則會在各自進程中保持一個資源緩沖。