安卓資源管理機制簡析

Android中,所有的資源都在res目錄下存放,包括drawable,layout,strings,anim等等,當我們向工程中加入任何一個資源時,會在R類中相應會爲該 資源分配一個id,我們在應用中就是通過這個id來訪問資源的,相信做過Andorid開發的朋友對於這些肯定不會陌生,所以這個也不是我今天想要說的,我今天想和大家一起學習的是android是如何管理資源的,在Android系統中,資源大部分都是通過xml文件定義的(drawable是圖片),如layout,string,anim都是xml文件,而對於layout,anim和strings等xml文件僅僅是解析xml文件,讀取指定的值而已,但是對於layout文件中控件的解析就比較複雜了,例如對於一個Button,需要解析它所有的屬性值,這個是如何實現的呢。


這裏我們首先要考慮一個問題,就是一個控件有哪些屬性是如何定義的?比如TextView具有哪些屬性?爲什麼我設置TextView的樣式只能用style而不能用android:theme?這些信息都是在哪裏定義的,想要弄清楚這個問題,就必須從源碼工程招答案,我使用的是android4.1工程,如果你使用的是其他版本的,那麼可能用些出入。

先看三個文件

1、d:\android4.1\frameworks\base\core\res\res\values\attrs.xml

看到attrs.xml文件,不知道你有沒有想起什麼?當我們在自定義控件的時候,是不是會創建一個attrs.xml文件?使用attrs.xml文件的目的其實就是給我們自定義的控件添加屬性,打開這個目錄後,你會看到定義了一個叫"Theme"的styleable,如下(我只截取部分)

[html] view plain copy
 print?
  1. <declare-styleable name="Theme">  
  2.         <!-- ============== -->  
  3.         <!-- Generic styles -->  
  4.         <!-- ============== -->  
  5.         <eat-comment />  
  6.   
  7.         <!-- Default color of foreground imagery. -->  
  8.         <attr name="colorForeground" format="color" />  
  9.         <!-- Default color of foreground imagery on an inverted background. -->  
  10.         <attr name="colorForegroundInverse" format="color" />  
  11.         <!-- Color that matches (as closely as possible) the window background. -->  
  12.         <attr name="colorBackground" format="color" />  


在這個文件中,定義了Android中大部分可以使用的屬性,這裏我說的是“定義”而不是“聲明”,同名在語法上面最大的區別就是定義要有format屬性,而聲明沒有format屬性。

2、d:\android4.1\frameworks\base\core\res\res\values\attrs_manifest.xml

這個文件的名字和上面的文件的名字很像,就是多了一個manifest,故名思議就是定義了AndroidManifest.xml文件中的屬性,這裏面有一個很重要的一句話

[html] view plain copy
 print?
  1. <attr name="theme" format="reference" />  

定義了一個theme屬性,這個就是我們平時在Activity上面使用的theme屬性

3、d:\android4.1\frameworks\base\core\res\res\values\themes.xml

這個文件開始定義了一個叫做"Theme" 的sytle,如下(截圖部分)

[html] view plain copy
 print?
  1. <style name="Theme">  
  2.   
  3.         <item name="colorForeground">@android:color/bright_foreground_dark</item>  
  4.         <item name="colorForegroundInverse">@android:color/bright_foreground_dark_inverse</item>  
  5.         <item name="colorBackground">@android:color/background_dark</item>  
  6.         <item name="colorBackgroundCacheHint">?android:attr/colorBackground</item>  

這個就是我們平時在Application或者Activity中使用的Theme,從這裏可以看出,Theme也是一種style,那爲什麼style只能永遠View/ViewGorup,而Theme只能用於Activity或者Application呢?先記住此問題,我們後續會爲你解答


我們再來整合這三個文件的內容吧,首先在attrs.xml文件中,定義了Android中大部分的屬性,也就是說以後所有View/Activity中大部分的屬性就是在這裏定義的,然後在attrs_manifest.xml中定義了一個叫做theme的屬性,它的值就是再themes文件中定義的Theme或者繼承自“Theme”的style。


有了上面的知識後,我們再來分析上面說過的兩個問題:

1、TextView控件(其他控件也一樣)的屬性在哪裏定義的。

2、既然Theme也是style,那爲什麼View只能用style,Activity只能使用theme?


所有View的屬性定義都是在attrs.xml文件中的,所以我們到attrs.xml文件中尋找TextView的styleable吧

[html] view plain copy
 print?
  1. <declare-styleable name="TextView">  
  2.        <!-- Determines the minimum type that getText() will return.  
  3.             The default is "normal".  
  4.             Note that EditText and LogTextBox always return Editable,  
  5.             even if you specify something less powerful here. -->  
  6.        <attr name="bufferType">  
  7.            <!-- Can return any CharSequence, possibly a  
  8.             Spanned one if the source text was Spanned. -->  
  9.            <enum name="normal" value="0" />  
  10.            <!-- Can only return Spannable. -->  
  11.            <enum name="spannable" value="1" />  
  12.            <!-- Can only return Spannable and Editable. -->  
  13.            <enum name="editable" value="2" />  
  14.        </attr>  
  15.        <!-- Text to display. -->  
  16.        <attr name="text" format="string" localization="suggested" />  
  17.        <!-- Hint text to display when the text is empty. -->  
  18.        <attr name="hint" format="string" />  
  19.        <!-- Text color. -->  
  20.        <attr name="textColor" />  


上面的屬性我只截取了部分,請注意,這裏所有的屬性都是進行“聲明”,你去搜索這個styleable,會發現在TextView的styleable中不會找到theme這個屬性的聲明,所以你給任何一個view設置theme屬性是沒有效果的。請看下面一段代碼就知道爲什麼了。

定義一個attrs.xml

[html] view plain copy
 print?
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <declare-styleable name="MyTextView">  
  4.         <attr name="orientation">  
  5.             <enum name="horizontal" value="0" />  
  6.             <enum name="vertical" value="1" />  
  7.         </attr>  
  8.     </declare-styleable>  
  9. </resources>  
定義一個MyTextView

[java] view plain copy
 print?
  1. public class MyTextView extends TextView {  
  2.   private static final String TAG = "MyTextView";  
  3.   public MyTextView(Context context)   
  4.   {  
  5.     super(context);  
  6.   }  
  7.   public MyTextView(Context context, AttributeSet attrs)   
  8.   {  
  9.     super(context, attrs);  
  10.     //利用TypeArray讀取自定義的屬性  
  11.     TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MyTextView);  
  12.     String value=ta.getString(R.styleable.MyTextView_orientation);  
  13.     Log.d("yzy""value1--->"+value);  
  14.     ta.recycle();  
  15.   }  
  16. }  


在attrs.xml我爲MyTextView定義了一個orientation屬性,然後再MyTextView的構造函數中去讀取這個屬性,這裏就涉及到TypeArray這個類,我們發現得到TypeArray需要傳入R.style.MyTextView這個值,這個就是系統爲我們訪問MyTextView這個styleable提供的一個id,當我們需要拿到orientation這個屬性的值時,我們通過R.style.MyTextView_orientation拿到,由於MyTextView中沒有定義或者聲明theme屬性,所以我們找不到R.styleable.MyTextView_theme這個id,所以導致我們無法解析它的theme屬性。同樣回到TextView這個styleable來,由於TextView的styleable中沒有定義theme屬性,所以theme對於TextView是沒有用的。所以即使你在TextView裏面加入theme屬性,即使編譯器不會給你報錯,這個theme也是被忽略了的。


我們再來看看Activity的屬性是如何定義的,由於Activity是在AndroidManigest.xml文件中定義的,所以我們到attrs_manifest.xml中查找。

[html] view plain copy
 print?
  1. <declare-styleable name="AndroidManifestActivity" parent="AndroidManifestApplication">  
  2.     <!-- Required name of the class implementing the activity, deriving from  
  3.         {@link android.app.Activity}.  This is a fully  
  4.         qualified class name (for example, com.mycompany.myapp.MyActivity); as a  
  5.         short-hand if the first character of the class  
  6.         is a period then it is appended to your package name. -->  
  7.     <attr name="name" />  
  8.     <attr name="theme" />  
  9.     <attr name="label" />  
  10.     <attr name="description" />  
  11.     <attr name="icon" />  
  12.     <attr name="logo" />  
  13.     <attr name="launchMode" />  
  14.     <attr name="screenOrientation" />  
  15.     <attr name="configChanges" />  
  16.     <attr name="permission" />  
  17.     <attr name="multiprocess" />  
  18.     <attr name="process" />  
  19.     <attr name="taskAffinity" />  
  20.     <attr name="allowTaskReparenting" />  
  21.     <attr name="finishOnTaskLaunch" />  
  22.     <attr name="finishOnCloseSystemDialogs" />  
  23.     <attr name="clearTaskOnLaunch" />  
  24.     <attr name="noHistory" />  
  25.     <attr name="alwaysRetainTaskState" />  
  26.     <attr name="stateNotNeeded" />  
  27.     <attr name="excludeFromRecents" />  
  28.     <!-- Specify whether the activity is enabled or not (that is, can be instantiated by the system).  
  29.          It can also be specified for an application as a whole, in which case a value of "false"  
  30.          will override any component specific values (a value of "true" will not override the  
  31.          component specific values). -->  
  32.     <attr name="enabled" />  
  33.     <attr name="exported" />  
  34.     <!-- Specify the default soft-input mode for the main window of  
  35.          this activity.  A value besides "unspecified" here overrides  
  36.          any value in the theme. -->  
  37.     <attr name="windowSoftInputMode" />  
  38.     <attr name="immersive" />  
  39.     <attr name="hardwareAccelerated" />  
  40.     <attr name="uiOptions" />  
  41.     <attr name="parentActivityName" />  
  42. </declare-styleable>  


很明顯,Activity對於的styleable中是聲明瞭theme的,所以它可以解析theme屬性。


上面兩個問題都已經解答完了,下面來討論另一個話題,就是Resources的獲取過程。

在我的另外一篇文章曾經討論過這個話題更深層次理解Context 這裏我們再來學習一下Resources的獲取過程。


在Android系統中,獲取Resources主要有兩種方法,通過Context獲取和PackageManager獲取

首先,我們看看我們通過Context獲取,下面這張圖是Context相關類的類圖


從圖中可以看出,Context有兩個子類,一個是ContextWrapper,另一個是ContextImpl,而ContextWrapper依賴於ContextImpl。結合源碼,我們會發現,Context是一個抽象類,它的真正實現類就是ContextImpl,而ContextWrapper就像他的名字一樣,僅僅是對Context的一層包裝,它的功能都是通過調用屬性mBase完成,該mBase實質就是指向一個ContextImpl類型的變量。我們獲取Resources時就是調用Context的getResources方法,那麼我們直接看看ContextImpl的getResources方法吧

[java] view plain copy
 print?
  1. @Override  
  2.    public Resources getResources() {  
  3.        return mResources;  
  4.    }  


我們發現這個方法很簡單,就是返回mResources屬性,那麼這個屬性是在哪裏 賦值的呢,通過尋找發現,其實就是在創建ContextImpl,通過調用Init進行賦值的(具體邏輯參照《更深層次理解Context》).這裏我先給出getResource方法的時序圖,然後跟蹤源碼。


先從init方法開始吧

[java] view plain copy
 print?
  1. final void init(LoadedApk packageInfo,  
  2.                 IBinder activityToken, ActivityThread mainThread,  
  3.                 Resources container, String basePackageName) {  
  4.         mPackageInfo = packageInfo;  
  5.         mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;  
  6.         mResources = mPackageInfo.getResources(mainThread);  
  7.   
  8.         if (mResources != null && container != null  
  9.                 && container.getCompatibilityInfo().applicationScale !=  
  10.                         mResources.getCompatibilityInfo().applicationScale) {  
  11.             if (DEBUG) {  
  12.                 Log.d(TAG, "loaded context has different scaling. Using container's" +  
  13.                         " compatiblity info:" + container.getDisplayMetrics());  
  14.             }  
  15.             mResources = mainThread.getTopLevelResources(  
  16.                     mPackageInfo.getResDir(), container.getCompatibilityInfo());  
  17.         }  
  18.         mMainThread = mainThread;  
  19.         mContentResolver = new ApplicationContentResolver(this, mainThread);  
  20.   
  21.         setActivityToken(activityToken);  
  22.     }  


我們發現,對mResource進行賦值,是通過調用LoadedApk中的getResource進行的,傳入了ActivityThead類型的參數

[java] view plain copy
 print?
  1. public Resources getResources(ActivityThread mainThread) {  
  2.       if (mResources == null) {  
  3.           mResources = mainThread.getTopLevelResources(mResDir, this);  
  4.       }  
  5.       return mResources;  
  6.   }  

在getResources方法中,其實就是調用了ActivityThrad的getTopLevelResources方法,其中mResDir就是apk文件的路徑(對於用戶安裝的app,此路徑就在/data/app下面的某一個apk),從時序圖中可以知道,getTopLevelResources其實就是調用了一個同名方法,我們直接看它的同名方法吧

[java] view plain copy
 print?
  1. Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {  
  2.         ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);  
  3.         Resources r;  
  4.         synchronized (mPackages) {  
  5.             // Resources is app scale dependent.  
  6.             if (false) {  
  7.                 Slog.w(TAG, "getTopLevelResources: " + resDir + " / "  
  8.                         + compInfo.applicationScale);  
  9.             }  
  10.             WeakReference<Resources> wr = mActiveResources.get(key);  
  11.             r = wr != null ? wr.get() : null;  
  12.             //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());  
  13.             if (r != null && r.getAssets().isUpToDate()) {  
  14.                 if (false) {  
  15.                     Slog.w(TAG, "Returning cached resources " + r + " " + resDir  
  16.                             + ": appScale=" + r.getCompatibilityInfo().applicationScale);  
  17.                 }  
  18.                 return r;  
  19.             }  
  20.         }<span style="font-family: Arial, Helvetica, sans-serif;">;</span>  
  21.   
  22.   
  23.         //if (r != null) {  
  24.         //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "  
  25.         //            + r + " " + resDir);  
  26.         //}  
  27.   
  28.         AssetManager assets = new AssetManager();  
  29.         if (assets.addAssetPath(resDir) == 0) {  
  30.             return null;  
  31.         }  
  32.   
  33.         //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);  
  34.         DisplayMetrics metrics = getDisplayMetricsLocked(nullfalse);  
  35.         r = new Resources(assets, metrics, getConfiguration(), compInfo);  
  36.         if (false) {  
  37.             Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "  
  38.                     + r.getConfiguration() + " appScale="  
  39.                     + r.getCompatibilityInfo().applicationScale);  
  40.         }  
  41.           
  42.         synchronized (mPackages) {  
  43.             WeakReference<Resources> wr = mActiveResources.get(key);  
  44.             Resources existing = wr != null ? wr.get() : null;  
  45.             if (existing != null && existing.getAssets().isUpToDate()) {  
  46.                 // Someone else already created the resources while we were  
  47.                 // unlocked; go ahead and use theirs.  
  48.                 r.getAssets().close();  
  49.                 return existing;  
  50.             }  
  51.               
  52.             // XXX need to remove entries when weak references go away  
  53.             mActiveResources.put(key, new WeakReference<Resources>(r));  
  54.             return r;  
  55.         }  
  56.     }  

這段代碼的邏輯不復雜,首先從mActiveResouuces中通過key拿到資源,如果資源不爲null,並且是最新的,那麼直接返回,否則創建一個AssetManager對象,並調用AssetManager的addAssetPath方法,然後使用創建的AssetManager爲參數,創建一個Resources對象,保存並返回。通過上面的時序圖,我們發現在創建AssetManager的時候,在其構造函數中調用init方法,我們看看init方法做了什麼吧

[java] view plain copy
 print?
  1. private native final void init();  


居然是一個本地方法,那麼我們只有看看對應的Jni代碼了

  1. static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)  
  2. {  
  3.     AssetManager* am = new AssetManager();  
  4.     if (am == NULL) {  
  5.         jniThrowException(env, "java/lang/OutOfMemoryError""");  
  6.         return;  
  7.     }  
  8.   
  9.     am->addDefaultAssets();  
  10.   
  11.     ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);  
  12.     env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);  
  13. }  

這個裏面調用了本地的AssetManager的addDefaultAssets方法

  1. bool AssetManager::addDefaultAssets()  
  2. {  
  3.     const char* root = getenv("ANDROID_ROOT");  
  4.     LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");  
  5.   
  6.     String8 path(root);  
  7.     path.appendPath(kSystemAssets);  
  8.   
  9.     return addAssetPath(path, NULL);  
  10. }  


這例的ANDROID_ROOT保存的就是/system路徑,而kSystemAssets是 

  1. static const char* kSystemAssets = "framework/framework-res.apk";  


還記得framework-res.apk是什麼嗎,就是系統所有的資源文件。

到這裏終於明白了,原理就是將系統的資源加載進來。


接下來看看addAssetPath方法吧,進入源碼後,你會發現它也是一個本地方法,也需要看jni代碼

  1. static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,  
  2.                                                        jstring path)  
  3. {  
  4.     ScopedUtfChars path8(env, path);  
  5.     if (path8.c_str() == NULL) {  
  6.         return 0;  
  7.     }  
  8.   
  9.     AssetManager* am = assetManagerForJavaObject(env, clazz);  
  10.     if (am == NULL) {  
  11.         return 0;  
  12.     }  
  13.   
  14.     void* cookie;  
  15.     bool res = am->addAssetPath(String8(path8.c_str()), &cookie);  
  16.   
  17.     return (res) ? (jint)cookie : 0;  
  18. }  

這裏調用了本地AssetManager方法的addAssetPath方法。和系統資源一樣,都被加載進來了。


下面看看PackageManager獲取Resource的流程吧

在PackageManager裏面獲取資源調用的是getResourcesForApplication方法,getResourcesForApplication也有一個同名方法,我們看辦正事的那個吧,

[java] view plain copy
 print?
  1. @Override public Resources getResourcesForApplication(  
  2.     ApplicationInfo app) throws NameNotFoundException {  
  3.     if (app.packageName.equals("system")) {  
  4.         return mContext.mMainThread.getSystemContext().getResources();  
  5.     }  
  6.     Resources r = mContext.mMainThread.getTopLevelResources(  
  7.         app.uid == Process.myUid() ? app.sourceDir  
  8.         : app.publicSourceDir, mContext.mPackageInfo);  
  9.     if (r != null) {  
  10.         return r;  
  11.     }  
  12.     throw new NameNotFoundException("Unable to open " + app.publicSourceDir);  
  13. }  
首先判斷包名是否是system,如果不是那麼直接調用ActivityThread的getTopLevelResources方法。不過這裏會根據當前應用的應用的uid和進程Id相等,如果相等則傳入app.sourceDir,否則傳入publicSourceDir,但是根據經驗時期sourceDir和publicSource一般情況下是相同的。後面的邏輯和Context中的是一樣的,這裏就不在說了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章