Android 資源加載機(jī)制詳解

Android提供了一種非常靈活的資源系統(tǒng),可以根據(jù)不同的條件提供可替代資源。因此,系統(tǒng)基于很少的改造就能支持新特性,比如Android N中的分屏模式。這也是Android強(qiáng)大部分之一。本文主要講述Android資源系統(tǒng)的實現(xiàn)原理,以及在應(yīng)用開發(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相當(dāng)于一個屬性集合,其在R.java文件中對應(yīng)一個int[]數(shù)組,aapt為styleable中的每個attr(屬性)分配一個id值,int[]中的每個id對應(yīng)著styleable中的每一個attr。

對于<declare-styleable name="Window">,Window相當(dāng)于屬性集合的名稱。
對于<attr name="windowBackground">,windowBackground相當(dāng)于屬性的名稱;屬性名稱在應(yīng)用程序范圍內(nèi)必須唯一,既無論定義幾個資源文件,無論定義幾個styleable,windowBackground必須唯一。

在Java代碼中,變量在一個作用域內(nèi)只能聲明一次,但可以多次使用。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,純粹是一個輔助類,當(dāng)從XML文件解析時會返回AttributeSet對象,該對象包含了解析元素的所有屬性及屬性值。并且在解析的屬性名稱與attrs.xml中定義的屬性名稱之間建立聯(lián)系。AttributeSet還提供了一組API接口從而可以方便的根據(jù)attrs.xml中已有的名稱獲取相應(yīng)的值。

如果使用一般的XML解析工具,則可以通過類似getElementById()等方法獲取屬性的名稱和屬性值,然而這樣并沒有在獲取的屬性名稱與attrs.xml定義的屬性名稱之間建立聯(lián)系。

Attribute對象一般作為View的構(gòu)造函數(shù)的參數(shù)傳遞過來,例如:

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屬性對應(yīng)的字符串,此處返回"@+id/tv"
  • public String getStyleAttribute(),獲取style屬性對應(yīng)的字符串,返回"@style/text"
  • public int getIdAttributeResourceValue(int defaultValue),返回id屬性對應(yīng)的int值,此處對應(yīng)R.id.tv。

第二類,操作通用屬性:

  • public int getAttributeCount(),獲取屬性的數(shù)目,本例中返回4
  • public String getAttributeName(int index),根據(jù)屬性所在位置返回相應(yīng)的屬性名稱。例如,id=0,layout_width=1,layout_height=2,style=3,如果getAttributeName(2),則返回android:layout_height
  • public String getAttributeValue(int index),根據(jù)位置返回值。本例中,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)對應(yīng)的數(shù)據(jù)類型,否則會返回錯誤。而且該方法僅適用于特定的類型,如果某個屬性值為一個style類型,或者為一個layout類型,那么返回值都將無效。

TypedArray

程序員在開發(fā)應(yīng)用程序時,在XML文件中引用某個變量通常是android:background="@drawable/background",該引用對應(yīng)的元素一般為某個View/ViewGroup,而View/ViewGroup的構(gòu)造函數(shù)中會通過obatinStyledAttributes方法返回一個TypedArray對象,然后再調(diào)用對象中的getDrawable()方法獲取背景圖片。

TypedArray是對AttributeSet數(shù)據(jù)類的某種抽象。對于andorid:layout_width="@dimen/width",如果使用AttributeSet的方法,僅僅能獲取"@dimen/width"字符串。而實際上該字符串對應(yīng)了一個dimen類型的數(shù)據(jù)。TypedArray可以將某個AttributeSet作為參數(shù)構(gòu)造TypedArray對象,并提供更方便的方法直接獲取該dimen的值。

TypedArray a = context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.XXX,defStyle,0);

方法obtainStyledAttributes()的第一個參數(shù)是一個AttributeSet對象,它包含了一個XML元素中定義的所有屬性。第二個參數(shù)是前面定義的styleable,appt會把一個styleable編譯成一個int[]數(shù)組,該數(shù)組的內(nèi)部實現(xiàn)正是通過遍歷AttributeSet中的每一個屬性,找到用戶感興趣的屬性,然后把值和屬性經(jīng)過重定位,返回一個TypedArray對象。想要獲取某個屬性的值則調(diào)用相關(guān)的方法即可,比如TypedArray.getDrawbale(),TypedArray.getString()等。getDrawable(),getString()方法內(nèi)部均通過Resources獲取屬性值。

加載資源


在使用資源時首先要把資源加載到內(nèi)存。Resources的作用主要就是加載資源,應(yīng)用程序需要的所有資源(包括系統(tǒng)資源)都是通過此對象獲取。一般情況下每個應(yīng)用都會僅有一個Resources對象。

要訪問資源首先要獲取Resources對象。獲取Resources對象有兩種方法,一種是通過Context,一種是通過PackageManager。

使用Context獲取Resources

抽象類Context內(nèi)部個有g(shù)etResources()方法,一般是在Activity對象或者Service對象中調(diào)用,因為Activity或者Service的本質(zhì)是一個Context,而真正實現(xiàn)Context接口的是ContextImpl類。

獲取Resources對象流程圖

ContextImpl對象是在ActivityThread類中創(chuàng)建,所以getResources()方法實際上是調(diào)用ContextImpl.getResources()方法。在ContextImpl類中,該方法僅僅是返回內(nèi)部的mResources變量,而對該變量賦值是在init()方法中。在創(chuàng)建ContextImpl對象后,一般會調(diào)用init()方法對ContextImpl對象內(nèi)部變量初始化,其中就包括mResources變量,如以下代碼所示:

final void init(ActivityThread.PackageInfo packageInfo, IBinder activityToken, ActivityThread mainThread, Resources container){
  mPackageInfo = packageInfo;
  mResources = mPackageInfo.getResources(mainThread);
}

從以上代碼可以看出,mResources又是調(diào)用mPackageInfo的getResources()方法進(jìn)行賦值。一個應(yīng)用程序中可以有多個ContextImpl,但多個ContextImpl對象共享一個PackageInfo對象。所以多個ContextImpl對象中的mResources變量實際上是同一個Resources對象。

PackageInfo.getResources()方法如下所示:

public Resources getResources(ActivityThread mainThread){
  if(mResources == null){
    mResources = mainThread.getTopLevelResources(mResDir,this);
  }
}

以上代碼中,參數(shù)mainThread指的就是ActivityThread對象,每個應(yīng)用程序只有一個ActivityThread對象。getTopLevelResources()方法就是獲取本應(yīng)用程序中的Resources對象。

在ActivityThread對象中,使用HashMap<ResourcesKey,WeakReference<Resources>> mActiveResources保存該應(yīng)用程序所有的Resources對象,并且這些Resources都是以一個弱引用保存起來的,這樣在內(nèi)存緊張時可以釋放Resources所占的內(nèi)存。

在mActiveResources中,使用ResourcesKey映射Resources類,ResourcesKey僅僅是一個數(shù)據(jù)類,其創(chuàng)建方式如下所示:

ResourcesKey key = new ResourcesKey(resDir,compInfo.applicatioScale);

resDir變量代表資源文件所在路徑,實際是指APK程序所在路徑,例如 /data/app/xxx.apk。該APK會對應(yīng)/data/dalvik-cache目錄下的data@app@xxx.apk@classes.dex文件,這兩個文件也是應(yīng)用程序安裝后自動生成的文件。

如果一個應(yīng)用程序沒有訪問該應(yīng)用程序以外的資源,那么mActivieResources變量中就僅有一個Resources對象。當(dāng)應(yīng)用程序想要訪問其他應(yīng)用程序的資源則需要構(gòu)建不同的ResourcesKey,也就是需要不同的resDir,畢竟每一個ResourcesKey對應(yīng)一個Resources對象,這樣該應(yīng)用程序就可以訪問其他應(yīng)用程序中的資源。

如果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ā)應(yīng)用程序時,使用Resources.getAssets()獲取的就是這里創(chuàng)建的AssetManager對象。AssetManager其實并不只是訪問res/assets目錄下的資源,而是可以訪問res目錄下的所有資源。

AssetManager在初始化的時候會被賦予兩個路徑,一個是應(yīng)用程序資源路徑 /data/app/xxx.apk,一個是Framework資源路徑/system/framework/framework-res.apk(系統(tǒng)資源會被打包到此apk中)。所以應(yīng)用程序使用本地Resources既可訪問應(yīng)用程序資源,又可訪問系統(tǒng)資源。

AssetManager中很多獲取資源的關(guān)鍵方法都是native實現(xiàn),當(dāng)使用getXXX(int id)訪問資源時,如果id小于0x1000 0000時表示訪問系統(tǒng)資源,如果id都大于0x7000 0000則表示應(yīng)用資源。aapt在對系統(tǒng)資源進(jìn)行編譯時,所有資源id都被編譯為小于0x1000 0000。

當(dāng)創(chuàng)建好Resources后就把該對象放到mActivieResources中以便以后繼續(xù)使用。

使用PackageManager獲取Resources

該方法主要是用來訪問其他應(yīng)用程序中的資源,最典型的就是切換主題,但這種主題一般僅限于一個應(yīng)用程序內(nèi)部。獲取Resources的過程如下所示:


使用PackageManager獲取Resources對象流程

使用PackageManager獲取Resources對象:

PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.android...your package name");

其中g(shù)etPackageManager()返回一個PackageManager對象,PackageManager本身是一個abstract類,其真正實現(xiàn)類是ApplicationPackageManager。其內(nèi)部方法一般調(diào)用遠(yuǎn)程PackageManagerService。ApplicationPackageManager在構(gòu)造時傳入一個遠(yuǎn)程服務(wù)的引用IPackageManager,該對象是通過調(diào)用getPackageManager()靜態(tài)方法獲取的。這種獲取遠(yuǎn)程服務(wù)的方法和大多數(shù)獲取遠(yuǎn)程服務(wù)的方法類似:

public static IPackageManager getPackageManager(){
  if(sPackageManager !=null){
    return sPackageManager;
  }
  IBinder b = ServiceManager.getService("package");
  sPackageManager = IPackageManager.Stub.asInterface(b);
  return sPackageManager;
}

獲得了PackageManager對象后,接著調(diào)用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);
}

以上代碼內(nèi)部調(diào)用mMainThread.getTopLevelResources()方法,又回到了使用Context獲取Resources對象的過程中。注意,此處調(diào)用參數(shù)的含義:如果目標(biāo)資源程序和當(dāng)前程序是同一個uid,那么就使用目標(biāo)程序的sourceDir作為路徑,否則就使用目標(biāo)程序的publicSourceDir目錄,該目錄可以在AndroidManifest.xml中指定。在大多數(shù)情況下,目標(biāo)程序和當(dāng)前程序不屬于同一個uid,因此,多為publicSourceDir,而該值默認(rèn)情況下和sourceDir的值相同。

當(dāng)進(jìn)入mMainThread.getTopLevelResources()方法后,ActivityThread對象就會在mActivieResources變量中保存一個新的Resources對象,其鍵值對應(yīng)目標(biāo)程序的包名。

加載應(yīng)用程序資源

應(yīng)用程序打包的最終文件是xxx.apk。APK本身是一個zip文件,可以使用壓縮工具解壓。系統(tǒng)在安裝應(yīng)用程序時首先解壓,并將其中的文件放到指定目錄。其中有一個文件名為resources.arsc,APK所有的資源均在其中定義。

resources.arsc是一種二進(jìn)制格式的文件。aapt在對資源文件進(jìn)行編譯時,會為每一個資源分配唯一的id值,程序在執(zhí)行時會根據(jù)這些id值讀取特定的資源,而resources.arsc文件正是包含了所有id值得一個數(shù)據(jù)集合。在該文件中,如果某個id對應(yīng)的資源是String或者數(shù)值(包括int,long等),那么該文件會直接包含相應(yīng)的值,如果id對應(yīng)的資源是某個layout或者drawable資源,那么該文件會存入對應(yīng)資源的路徑地址。

事實上,當(dāng)程序運(yùn)行時,所需要的資源都要從原始文件中讀取(APK在安裝時都會被系統(tǒng)拷貝到/data/app目錄下)。加載資源時,首先加載resources.arsc,然后根據(jù)id值找到指定的資源。

加載Framework資源
系統(tǒng)資源是在zygote進(jìn)程啟動時被加載的,并且只有當(dāng)加載了系統(tǒng)資源后才開始啟動其他應(yīng)用進(jìn)程,從而實現(xiàn)其他應(yīng)用進(jìn)程共享系統(tǒng)資源的目標(biāo)。

啟動第一步就是加載系統(tǒng)資源,加載完畢后再調(diào)用startSystemServer()啟動系統(tǒng)進(jìn)程,并最后調(diào)用runSelectLoopMode()開始監(jiān)聽Socket,并啟動指定的應(yīng)用進(jìn)程。加載系統(tǒng)資源是通過preLoadResources()完成的,該方法關(guān)鍵代碼如下所示:

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對象,一般情況下應(yīng)用程序不應(yīng)該調(diào)用此方法,因為該方法返回的Resources僅能訪問Framework資源。

當(dāng)Resources對象創(chuàng)建完成后,調(diào)用preloadDrawables()和preloadColorStateLists()裝在需要"預(yù)裝載"的資源。這兩個方法都需要傳入一個TypeArray,其來源是res/values/arrays.xml中定義的一個array數(shù)組資源,例如:

<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類中,相關(guān)資源讀取函數(shù)需要將讀取到的資源緩沖起來,以便以后使用,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對象均共享這三個變量。所以當(dāng)應(yīng)用程序創(chuàng)建新的Resources對象時可以訪問系統(tǒng)資源。

第四個變量用來區(qū)分是zygote裝在資源還是普通應(yīng)用進(jìn)程裝在資源。因為zygote與普通進(jìn)程裝載資源的方式類似,所以增加mPreloaded變量進(jìn)行區(qū)分。

mPreloaded在startPreloading()中被置為true,在finishPreloading()中被置為false,而startPreloading()和finishPreloading()正是在ZygoteInit.java的preloadResources()中被調(diào)用,這就區(qū)別了zygote調(diào)用和普通進(jìn)程調(diào)用。

最后,在Resources的具體資源讀取方法中,會判斷mPreloaded變量,如果為true,則同時把讀取到的資源存儲到三個靜態(tài)列表中,否則把資源放到非靜態(tài)列表中,這些非靜態(tài)列表的作用范圍為調(diào)用者所在進(jìn)程。

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中預(yù)先定義的資源值,F(xiàn)ramework包含了更多的資源,zygote所加載的僅僅是一小部分。對于那些非"預(yù)裝載"的系統(tǒng)資源則不會被緩沖到靜態(tài)列表變量中,這時應(yīng)用進(jìn)程如果需要一個非預(yù)裝載資源則會在各自進(jìn)程中保持一個資源緩沖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,893評論 25 709
  • Android中View自定義XML屬性詳解以及R.attr與R.styleable的區(qū)別 Android中的各種...
    毹毹閱讀 7,953評論 0 11
  • 今天說一個前不久發(fā)生在我身邊所見所聞的事情,希望給大家一點啟示。 一天一位女士帶著自己的孩子在街道里玩,女士的孩子...
    吳石磊閱讀 650評論 0 4
  • 今天折騰了半天才把準(zhǔn)考證打印下來,刷刷知乎發(fā)現(xiàn)自己特別小格局,全是牛人,根本不能比。一個個都對自己特別狠,還有,看...
    檸檬安然閱讀 151評論 0 0

友情鏈接更多精彩內(nèi)容