Android提供了一種非常靈活的資源系統(tǒng),可以根據不同的條件提供可替代資源。因此,系統(tǒng)基于很少的改造就能支持新特性耘眨,比如Android N中的分屏模式。這也是Android強大部分之一境肾。本文主要講述Android資源系統(tǒng)的實現(xiàn)原理剔难,以及在應用開發(fā)中需要注意的事項。
定義資源
Android使用XML文件描述各種資源奥喻,包括字符串偶宫、顏色、尺寸环鲤、主題纯趋、布局、甚至是圖片(selector冷离,layer-list)吵冒。
資源可分為兩部分,一部分是屬性西剥,另一部分是值痹栖。對于android:text="hello,world"
瞭空,text
就是屬性揪阿,hello,world
就是值咆畏。
屬性的定義
在APK程序中南捂,屬性定義在res/values/attrs.xml
中,在系統(tǒng)中屬性位于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中定義的屬性名稱之間建立聯(lián)系。AttributeSet還提供了一組API接口從而可以方便的根據attrs.xml中已有的名稱獲取相應的值烘跺。
如果使用一般的XML解析工具湘纵,則可以通過類似getElementById()等方法獲取屬性的名稱和屬性值,然而這樣并沒有在獲取的屬性名稱與attrs.xml定義的屬性名稱之間建立聯(lián)系滤淳。
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鸯绿。前面也說過,系統(tǒng)會為每一個attr分配一個唯一的id簸淀。
第三類瓶蝴,獲取特定類型的值:
-
public XXXType getAttributeXXXType(int index,XXXType defaultValue)
,其中XXXType包括int租幕、unsigned int舷手、boolean、float類型令蛉。使用該方法時,必須明確知道某個位置(index)對應的數據類型,否則會返回錯誤珠叔。而且該方法僅適用于特定的類型蝎宇,如果某個屬性值為一個style類型,或者為一個layout類型祷安,那么返回值都將無效姥芥。
TypedArray
程序員在開發(fā)應用程序時,在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[]數組,該數組的內部實現(xiàn)正是通過遍歷AttributeSet中的每一個屬性逻锐,找到用戶感興趣的屬性夫晌,然后把值和屬性經過重定位,返回一個TypedArray對象谦去。想要獲取某個屬性的值則調用相關的方法即可慷丽,比如TypedArray.getDrawbale(),TypedArray.getString()等鳄哭。getDrawable()要糊,getString()方法內部均通過Resources獲取屬性值。
加載資源
在使用資源時首先要把資源加載到內存妆丘。Resources的作用主要就是加載資源锄俄,應用程序需要的所有資源(包括系統(tǒng)資源)都是通過此對象獲取。一般情況下每個應用都會僅有一個Resources對象勺拣。
要訪問資源首先要獲取Resources對象奶赠。獲取Resources對象有兩種方法,一種是通過Context药有,一種是通過PackageManager毅戈。
使用Context獲取Resources
抽象類Context內部個有getResources()方法苹丸,一般是在Activity對象或者Service對象中調用,因為Activity或者Service的本質是一個Context苇经,而真正實現(xiàn)Context接口的是ContextImpl類赘理。
ContextImpl對象是在ActivityThread類中創(chuàng)建,所以getResources()方法實際上是調用ContextImpl.getResources()方法扇单。在ContextImpl類中商模,該方法僅僅是返回內部的mResources變量,而對該變量賦值是在init()方法中蜘澜。在創(chuàng)建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僅僅是一個數據類鲜侥,其創(chuàng)建方式如下所示:
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對象篡撵,那就需要重新創(chuàng)建一個:
AssetManager assets = new AssetManager();
if(assets.addAssetPath(resDir) == 0){
return null;
}
DisplayMetrics metrics = getDisplayMetricsLocked(false);
r = new Resources(assets,metrics,getConfiguration(),compInfo);
創(chuàng)建Resources需要一個AssetManager對象判莉。在開發(fā)應用程序時,使用Resources.getAssets()獲取的就是這里創(chuàng)建的AssetManager對象育谬。AssetManager其實并不只是訪問res/assets目錄下的資源骂租,而是可以訪問res目錄下的所有資源。
AssetManager在初始化的時候會被賦予兩個路徑斑司,一個是應用程序資源路徑 /data/app/xxx.apk
,一個是Framework資源路徑/system/framework/framework-res.apk
(系統(tǒng)資源會被打包到此apk中)但汞。所以應用程序使用本地Resources既可訪問應用程序資源宿刮,又可訪問系統(tǒng)資源。
AssetManager中很多獲取資源的關鍵方法都是native實現(xiàn)私蕾,當使用getXXX(int id)訪問資源時僵缺,如果id小于0x1000 0000時表示訪問系統(tǒng)資源,如果id都大于0x7000 0000則表示應用資源踩叭。aapt在對系統(tǒng)資源進行編譯時磕潮,所有資源id都被編譯為小于0x1000 0000。
當創(chuàng)建好Resources后就把該對象放到mActivieResources中以便以后繼續(xù)使用容贝。
使用PackageManager獲取Resources
該方法主要是用來訪問其他應用程序中的資源自脯,最典型的就是切換主題,但這種主題一般僅限于一個應用程序內部斤富。獲取Resources的過程如下所示:
使用PackageManager獲取Resources對象:
PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.android...your package name");
其中getPackageManager()返回一個PackageManager對象膏潮,PackageManager本身是一個abstract類,其真正實現(xiàn)類是ApplicationPackageManager满力。其內部方法一般調用遠程PackageManagerService焕参。ApplicationPackageManager在構造時傳入一個遠程服務的引用IPackageManager,該對象是通過調用getPackageManager()靜態(tài)方法獲取的油额。這種獲取遠程服務的方法和大多數獲取遠程服務的方法類似:
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文件宛瞄,可以使用壓縮工具解壓浮禾。系統(tǒng)在安裝應用程序時首先解壓,并將其中的文件放到指定目錄份汗。其中有一個文件名為resources.arsc盈电,APK所有的資源均在其中定義。
resources.arsc是一種二進制格式的文件杯活。aapt在對資源文件進行編譯時匆帚,會為每一個資源分配唯一的id值,程序在執(zhí)行時會根據這些id值讀取特定的資源旁钧,而resources.arsc文件正是包含了所有id值得一個數據集合吸重。在該文件中,如果某個id對應的資源是String或者數值(包括int歪今,long等)嚎幸,那么該文件會直接包含相應的值,如果id對應的資源是某個layout或者drawable資源寄猩,那么該文件會存入對應資源的路徑地址鞭铆。
事實上,當程序運行時焦影,所需要的資源都要從原始文件中讀瘸邓臁(APK在安裝時都會被系統(tǒng)拷貝到/data/app
目錄下)。加載資源時斯辰,首先加載resources.arsc舶担,然后根據id值找到指定的資源。
加載Framework資源
系統(tǒng)資源是在zygote進程啟動時被加載的彬呻,并且只有當加載了系統(tǒng)資源后才開始啟動其他應用進程衣陶,從而實現(xiàn)其他應用進程共享系統(tǒng)資源的目標。
啟動第一步就是加載系統(tǒng)資源闸氮,加載完畢后再調用startSystemServer()啟動系統(tǒng)進程剪况,并最后調用runSelectLoopMode()開始監(jiān)聽Socket,并啟動指定的應用進程蒲跨。加載系統(tǒng)資源是通過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()創(chuàng)建Resources對象,一般情況下應用程序不應該調用此方法或悲,因為該方法返回的Resources僅能訪問Framework資源孙咪。
當Resources對象創(chuàng)建完成后堪唐,調用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類中定義了四個靜態(tài)變量緩沖這些資源:
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對象均共享這三個變量曙寡。所以當應用程序創(chuàng)建新的Resources對象時可以訪問系統(tǒng)資源。
第四個變量用來區(qū)分是zygote裝在資源還是普通應用進程裝在資源寇荧。因為zygote與普通進程裝載資源的方式類似,所以增加mPreloaded變量進行區(qū)分执隧。
mPreloaded在startPreloading()中被置為true揩抡,在finishPreloading()中被置為false,而startPreloading()和finishPreloading()正是在ZygoteInit.java的preloadResources()中被調用镀琉,這就區(qū)別了zygote調用和普通進程調用峦嗤。
最后,在Resources的具體資源讀取方法中屋摔,會判斷mPreloaded變量烁设,如果為true,則同時把讀取到的資源存儲到三個靜態(tài)列表中钓试,否則把資源放到非靜態(tài)列表中装黑,這些非靜態(tài)列表的作用范圍為調用者所在進程。
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中預先定義的資源值弓熏,F(xiàn)ramework包含了更多的資源恋谭,zygote所加載的僅僅是一小部分。對于那些非"預裝載"的系統(tǒng)資源則不會被緩沖到靜態(tài)列表變量中挽鞠,這時應用進程如果需要一個非預裝載資源則會在各自進程中保持一個資源緩沖疚颊。