Android中資源管理機制詳解

在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,須要解析它全部的屬性值,這個是如何實現的呢。java

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

先看三個文件cookie

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

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

<declare-styleable name="Theme">
        <!-- ============== -->
        <!-- Generic styles -->
        <!-- ============== -->
        <eat-comment />

        <!-- Default color of foreground imagery. -->
        <attr name="colorForeground" format="color" />
        <!-- Default color of foreground imagery on an inverted background. -->
        <attr name="colorForegroundInverse" format="color" />
        <!-- Color that matches (as closely as possible) the window background. -->
        <attr name="colorBackground" format="color" />

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

二、d:\android4.1\frameworks\base\core\res\res\values\attrs_manifest.xml函數

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

<attr name="theme" format="reference" />

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

三、d:\android4.1\frameworks\base\core\res\res\values\themes.xmlthis

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

<style name="Theme">

        <item name="colorForeground">@android:color/bright_foreground_dark</item>
        <item name="colorForegroundInverse">@android:color/bright_foreground_dark_inverse</item>
        <item name="colorBackground">@android:color/background_dark</item>
        <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。

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

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

二、既然Theme也是style,那爲何View只能用style,Activity只能使用theme?

全部View的屬性定義都是在attrs.xml文件中的,因此咱們到attrs.xml文件中尋找TextView的styleable吧

 <declare-styleable name="TextView">
        <!-- Determines the minimum type that getText() will return.
             The default is "normal".
             Note that EditText and LogTextBox always return Editable,
             even if you specify something less powerful here. -->
        <attr name="bufferType">
            <!-- Can return any CharSequence, possibly a
             Spanned one if the source text was Spanned. -->
            <enum name="normal" value="0" />
            <!-- Can only return Spannable. -->
            <enum name="spannable" value="1" />
            <!-- Can only return Spannable and Editable. -->
            <enum name="editable" value="2" />
        </attr>
        <!-- Text to display. -->
        <attr name="text" format="string" localization="suggested" />
        <!-- Hint text to display when the text is empty. -->
        <attr name="hint" format="string" />
        <!-- Text color. -->
        <attr name="textColor" />

上面的屬性我只截取了部分,請注意,這裏全部的屬性都是進行「聲明」,你去搜索這個styleable,會發如今TextView的styleable中不會找到theme這個屬性的聲明,因此你給任何一個view設置theme屬性是沒有效果的。請看下面一段代碼就知道爲何了。

定義一個attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyTextView">
        <attr name="orientation">
            <enum name="horizontal" value="0" />
        	<enum name="vertical" value="1" />
        </attr>
    </declare-styleable>
</resources>

定義一個MyTextView

public class MyTextView extends TextView {
  private static final String TAG = "MyTextView";
  public MyTextView(Context context) 
  {
    super(context);
  }
  public MyTextView(Context context, AttributeSet attrs) 
  {
    super(context, attrs);
    //利用TypeArray讀取自定義的屬性
    TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
    String value=ta.getString(R.styleable.MyTextView_orientation);
    Log.d("yzy", "value1--->"+value);
    ta.recycle();
  }
}

在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中查找。

<declare-styleable name="AndroidManifestActivity" parent="AndroidManifestApplication">
    <!-- Required name of the class implementing the activity, deriving from
            {@link android.app.Activity}.  This is a fully
            qualified class name (for example, com.mycompany.myapp.MyActivity); as a
            short-hand if the first character of the class
            is a period then it is appended to your package name. -->
        <attr name="name" />
        <attr name="theme" />
        <attr name="label" />
        <attr name="description" />
        <attr name="icon" />
        <attr name="logo" />
        <attr name="launchMode" />
        <attr name="screenOrientation" />
        <attr name="configChanges" />
        <attr name="permission" />
        <attr name="multiprocess" />
        <attr name="process" />
        <attr name="taskAffinity" />
        <attr name="allowTaskReparenting" />
        <attr name="finishOnTaskLaunch" />
        <attr name="finishOnCloseSystemDialogs" />
        <attr name="clearTaskOnLaunch" />
        <attr name="noHistory" />
        <attr name="alwaysRetainTaskState" />
        <attr name="stateNotNeeded" />
        <attr name="excludeFromRecents" />
<!-- Specify whether the activity is enabled or not (that is, can be instantiated by the system).
It can also be specified for an application as a whole, in which case a value of "false"
will override any component specific values (a value of "true" will not override the
component specific values). -->
        <attr name="enabled" />
        <attr name="exported" />
        <!-- Specify the default soft-input mode for the main window of
             this activity.  A value besides "unspecified" here overrides
             any value in the theme. -->
        <attr name="windowSoftInputMode" />
        <attr name="immersive" />
        <attr name="hardwareAccelerated" />
        <attr name="uiOptions" />
        <attr name="parentActivityName" />
</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方法吧

@Override
public Resources getResources() {
    return mResources;
}

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

先從init方法開始吧

final void init(LoadedApk packageInfo,
                IBinder activityToken, ActivityThread mainThread,
                Resources container, String basePackageName) {
        mPackageInfo = packageInfo;
        mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;
        mResources = mPackageInfo.getResources(mainThread);

        if (mResources != null && container != null
                && container.getCompatibilityInfo().applicationScale !=
                        mResources.getCompatibilityInfo().applicationScale) {
            if (DEBUG) {
                Log.d(TAG, "loaded context has different scaling. Using container's" +
                        " compatiblity info:" + container.getDisplayMetrics());
            }
            mResources = mainThread.getTopLevelResources(
                    mPackageInfo.getResDir(), container.getCompatibilityInfo());
        }
        mMainThread = mainThread;
        mContentResolver = new ApplicationContentResolver(this, mainThread);

        setActivityToken(activityToken);
}

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

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

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

Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
        ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
        Resources r;
        synchronized (mPackages) {
            // Resources is app scale dependent.
            if (false) {
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / "
                        + compInfo.applicationScale);
            }
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (false) {
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                }
                return r;
            }
        }<span style="font-family: Arial, Helvetica, sans-serif;">;</span>


        //if (r != null) {
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "
        //            + r + " " + resDir);
        //}

        AssetManager assets = new AssetManager();
        if (assets.addAssetPath(resDir) == 0) {
            return null;
        }

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics metrics = getDisplayMetricsLocked(null, false);
        r = new Resources(assets, metrics, getConfiguration(), compInfo);
        if (false) {
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        }
        
        synchronized (mPackages) {
            WeakReference<Resources> wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) {
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            }
            
            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
    }
}

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

private native final void init();

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

static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
{
    AssetManager* am = new AssetManager();
    if (am == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", "");
        return;
    }

    am->addDefaultAssets();

    ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
    env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}

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

bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    path.appendPath(kSystemAssets);

    return addAssetPath(path, NULL);
}

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

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

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

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

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

static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
                                                       jstring path)
{
    ScopedUtfChars path8(env, path);
    if (path8.c_str() == NULL) {
        return 0;
    }

    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }

    void* cookie;
    bool res = am->addAssetPath(String8(path8.c_str()), &cookie);

    return (res) ? (jint)cookie : 0;
}

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

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

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

@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);
}

首先判斷包名是不是system,若是不是那麼直接調用ActivityThread的getTopLevelResources方法。不過這裏會根據當前應用的應用的uid和進程Id相等,若是相等則傳入app.sourceDir,不然傳入publicSourceDir,可是根據經驗時期sourceDir和publicSource通常狀況下是相同的。後面的邏輯和Context中的是同樣的,這裏就不在說了。

相關文章
相關標籤/搜索