外掛化知識梳理(9) 資源的動態載入示例及原始碼分析

澤毛發表於2017-12-21

一、前言

當需要設計一個外掛化的框架,首先需要解決的是以下三個問題:

  • Activity的動態註冊
  • 類的動態載入
  • 資源的動態載入

如果大家有閱讀過前面一系列的文章,那麼對於如何解決前兩個問題應該可以有一個大概的思路了。不清楚的可以重點看一下 外掛化知識梳理(6) - Small 原始碼分析之 Hook 原理外掛化知識梳理(8) - 類的動態載入原始碼分析。今天這篇,我就來先了解一下在Android當中資源是如何載入的。

二、示例

為了讓大家有一個直觀的認識,我們先不講原始碼,而是來看一個簡單的示例,該示例演示瞭如何以外掛的形式載入外部資源。

2.1 編譯外掛

這裡,我們需要將所需要的外掛資源放在一個.apk檔案中,因此,我們建立一個新的Phone & Tablet Module

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
在其中放入三個資原始檔:

  • drawable

    外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

  • string

<string name="resource_str">Plug Resources String</string>
複製程式碼
  • color
<color name="resource_color">#FF4081</color>
複製程式碼

我們將該Module編譯成為resource-debug.apk檔案,通過adb命令將它push到根目錄的Plugin/目錄下,至此,一個包含資源的外掛就準備好了。

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

2.2 讀取外掛中資源

現在,我們進入到宿主模組當中,讀取這三個資源並進行展示。程式碼很短,只有下面幾行:

    private void loadResource() {
        try {
            //新增資源路徑,並建立對應的Resources物件。
            String resourcePath = Environment.getExternalStorageDirectory().toString() + "/Plugin/resource-debug.apk";
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, resourcePath);
            Resources resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
            //獲取包名資訊。
            PackageInfo mInfo = getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
            //獲取到資源的ID。
            int drawableId = resources.getIdentifier("icon_book", "drawable", mInfo.packageName);
            int strId = resources.getIdentifier("resource_str", "string", mInfo.packageName);
            int colorId = resources.getIdentifier("resource_color", "color", mInfo.packageName);
            //通過資源ID獲取到對應的資源,並進行顯示。
            mImageView.setImageDrawable(resources.getDrawable(drawableId));
            mTextView.setText(resources.getText(strId));
            mTextView.setTextColor(resources.getColor(colorId));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
複製程式碼

上面的邏輯為以下幾步:

  • 獲取外掛的路徑,也就是在2.1中所push進去的resource-debug.apk所在的路徑。
  • 通過反射建立一個AssetManager物件,呼叫它的addAssetPath方法,該方法的實參為第一步中的外掛路徑。
  • 利用該AssetManager物件作為建構函式,建立一個訪問該外掛資源的代理物件resources,用於外掛資源的訪問。
  • 通過外掛的路徑,獲得外掛的包名資訊。
  • 通過resourcesgetIdentifier方法,根據外掛資源的名字以及外掛的包名獲取對應的資源Id
  • 通過resourcesgetXXX方法,傳入前一步中獲取到的資源Id,最終獲取資源,並通過控制元件進行展示。

最終的展示結果為:

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

三、原始碼解析

在第二節中,我們用一個簡單的例子,演示瞭如何以外掛的形式載入外部的資源,其實,無論是載入外部資源,還是載入宿主本身的資源,它們的原理都是相同的,只要我們弄懂了宿主自身的資源是如何載入的,那麼對於上面的過程自然也就理解了。

Android中,當我們需要載入一個資源時,一般都會先通過getResources()方法,得到一個Resources物件,再通過它提供的getXXX方法獲取到對應的資源,這一過程可以用下面這張圖來表示:

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

2.1 函式呼叫路徑

在上圖中,我們看到一共經過了四條線路呼叫到ResourcesManager,下面,我們就對這一過程進行分析:

第一步

當我們呼叫在Activity/Service/Application中呼叫getResources()時,由於它們都繼承於ContextWrapper,該方法就會呼叫到ContextWrappergetResources()方法,而該方法又會呼叫它內部的mBase變數的對應方法:

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

第二步

mBase的型別為ContextImpl,它的getResources()方法,返回的是其內部的成員變數mResources

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
mResources變數是在ContextImpl的建構函式中通過下面賦值的:
外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
其中packageInfo的型別為LoadedApkLoadedApk是對於.apk解析的結果,它內部包含了所關聯的ActivityThread,安裝後拷貝到的目錄,我們在ContextImpl中賦值的其實就是它內部的mResources物件:
外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

第三步

LoadedApk中會通過呼叫ActivityThreadgetTopLevelResources方法來為mResources變數賦值,在呼叫的時候會傳入LoadedApk中的一些資訊:

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
這裡最關鍵的是第一個變數mRes,它是應用安裝時拷貝到data/app/{package_name}下的完整路徑,其它的變數,大家可以參考斷點中的截圖,這裡就不多分析是怎麼獲得的了。
外掛化知識梳理(9)   資源的動態載入示例及原始碼分析

第四步

經過上面的步驟,最終會呼叫到ActivityThread中的getTopResources方法,在該方法中會通過ResourcesManager去尋找訪問資源的對應代理物件Resources

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
以上就是從Activity呼叫getResources()方法,到ResourcesManager根據應用的資訊去查詢訪問資源的代理物件的呼叫過程,下面,我們就來看一下ResourcesManager是如何管理這些Resources物件的。

2.2 ResourceManager

2.2.1 ResourceManager 對於 Resources 物件的管理

ResourceManager採用了單例的模式,因此一個程式當中只會有一個物件,它主要負責管理應用程式當中的Resources物件,在它的內部有以下兩個關鍵的成員變數:

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
當我們呼叫ResourcesManagergetResources方法之後,他就會根據傳入的引數建立一個ResourcesKey,並通過該物件中的屬性作為索引值,首先查詢在上面的快取中是否已經有對應的Resources物件了,如果有,那麼就直接返回,否則再建立一個Resources物件。

2.2.2 ResourcesKey && ResourcesImpl && Resources && AssetManager

Resources其實只是一個代理物件,它內部涉及到的類包括:

  • ResourcesKey:作為快取的Key,也就是說對於一個應用程式,可以儲存不同的Resource,是否返回之前的Resources物件,取決於ResourcesKeyequals方法是否相等:
    外掛化知識梳理(9)   資源的動態載入示例及原始碼分析
  • ResourcesImpl:資源訪問的實現類,其內部包含了一個AssetManager,所有資源的訪問都是通過它的Native方法來實現的。
  • ResourcesResourcesImpl的代理類,對於資源的使用者來說,看到的是Resources介面,其實在構建Resources物件時,同時也會建立一個ResourcesImpl物件作為它的成員變數,Resources會呼叫它來去獲取資源。
  • AssetManager:作為資源獲取的執行者,它是ResourcesImpl的內部成員變數。

也就是說,資源的訪問最終是由AssetManager來完成,在AssetManager的建立過程中我們首先告訴它資源所在的路徑,之後它就會去以下的幾個地方檢視資源,這裡面我們看到了第二節中通過反射呼叫的addAssetPath。動態載入資源的關鍵,就是如何把包含資源的外掛路徑新增到AssetManager當中。

外掛化知識梳理(9)   資源的動態載入示例及原始碼分析


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章