插件化知識梳理(9) - 資源的動態(tài)加載示例及源碼分析


相關(guān)閱讀

插件化知識梳理(1) - Small 框架之如何引入應(yīng)用插件
插件化知識梳理(2) - Small 框架之如何引入公共庫插件
插件化知識梳理(3) - Small 框架之宿主分身
插件化知識梳理(4) - Small 框架之如何實現(xiàn)插件更新
插件化知識梳理(5) - Small 框架之如何不將插件打包到宿主中
插件化知識梳理(6) - Small 源碼分析之 Hook 原理
插件化知識梳理(7) - 類的動態(tài)加載入門
插件化知識梳理(8) - 類的動態(tài)加載源碼分析
插件化知識梳理(9) - 資源的動態(tài)加載示例及源碼分析
插件化知識梳理(10) - Service 插件化實現(xiàn)及原理


一、前言

當需要設(shè)計一個插件化的框架,首先需要解決的是以下三個問題:

  • Activity的動態(tài)注冊
  • 類的動態(tài)加載
  • 資源的動態(tài)加載

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

二、示例

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

2.1 編譯插件

這里,我們需要將所需要的插件資源放在一個.apk文件中,因此,我們創(chuàng)建一個新的Phone & Tablet Module


在其中放入三個資源文件:

  • drawable

  • string

<string name="resource_str">Plug Resources String</string>
  • color
<color name="resource_color">#FF4081</color>

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

2.2 讀取插件中資源

現(xiàn)在,我們進入到宿主模塊當中,讀取這三個資源并進行展示。代碼很短,只有下面幾行:

    private void loadResource() {
        try {
            //添加資源路徑,并創(chuàng)建對應(yīng)的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獲取到對應(yīng)的資源,并進行顯示。
            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所在的路徑。
  • 通過反射創(chuàng)建一個AssetManager對象,調(diào)用它的addAssetPath方法,該方法的實參為第一步中的插件路徑。
  • 利用該AssetManager對象作為構(gòu)造函數(shù),創(chuàng)建一個訪問該插件資源的代理對象resources,用于插件資源的訪問。
  • 通過插件的路徑,獲得插件的包名信息。
  • 通過resourcesgetIdentifier方法,根據(jù)插件資源的名字以及插件的包名獲取對應(yīng)的資源Id
  • 通過resourcesgetXXX方法,傳入前一步中獲取到的資源Id,最終獲取資源,并通過控件進行展示。

最終的展示結(jié)果為:


三、源碼解析

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

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

2.1 函數(shù)調(diào)用路徑

在上圖中,我們看到一共經(jīng)過了四條線路調(diào)用到ResourcesManager,下面,我們就對這一過程進行分析:

第一步

當我們調(diào)用在Activity/Service/Application中調(diào)用getResources()時,由于它們都繼承于ContextWrapper,該方法就會調(diào)用到ContextWrappergetResources()方法,而該方法又會調(diào)用它內(nèi)部的mBase變量的對應(yīng)方法:

第二步

mBase的類型為ContextImpl,它的getResources()方法,返回的是其內(nèi)部的成員變量mResources


mResources變量是在ContextImpl的構(gòu)造函數(shù)中通過下面賦值的:

其中packageInfo的類型為LoadedApkLoadedApk是對于.apk解析的結(jié)果,它內(nèi)部包含了所關(guān)聯(lián)的ActivityThread,安裝后拷貝到的目錄,我們在ContextImpl中賦值的其實就是它內(nèi)部的mResources對象:

第三步

LoadedApk中會通過調(diào)用ActivityThreadgetTopLevelResources方法來為mResources變量賦值,在調(diào)用的時候會傳入LoadedApk中的一些信息:


這里最關(guān)鍵的是第一個變量mRes,它是應(yīng)用安裝時拷貝到data/app/{package_name}下的完整路徑,其它的變量,大家可以參考斷點中的截圖,這里就不多分析是怎么獲得的了。

第四步

經(jīng)過上面的步驟,最終會調(diào)用到ActivityThread中的getTopResources方法,在該方法中會通過ResourcesManager去尋找訪問資源的對應(yīng)代理對象Resources


以上就是從Activity調(diào)用getResources()方法,到ResourcesManager根據(jù)應(yīng)用的信息去查找訪問資源的代理對象的調(diào)用過程,下面,我們就來看一下ResourcesManager是如何管理這些Resources對象的。

2.2 ResourceManager

2.2.1 ResourceManager 對于 Resources 對象的管理

ResourceManager采用了單例的模式,因此一個進程當中只會有一個對象,它主要負責管理應(yīng)用程序當中的Resources對象,在它的內(nèi)部有以下兩個關(guān)鍵的成員變量:


當我們調(diào)用ResourcesManagergetResources方法之后,他就會根據(jù)傳入的參數(shù)創(chuàng)建一個ResourcesKey,并通過該對象中的屬性作為索引值,首先查找在上面的緩存中是否已經(jīng)有對應(yīng)的Resources對象了,如果有,那么就直接返回,否則再創(chuàng)建一個Resources對象。

2.2.2 ResourcesKey && ResourcesImpl && Resources && AssetManager

Resources其實只是一個代理對象,它內(nèi)部涉及到的類包括:

  • ResourcesKey:作為緩存的Key,也就是說對于一個應(yīng)用程序,可以保存不同的Resource,是否返回之前的Resources對象,取決于ResourcesKeyequals方法是否相等:
  • ResourcesImpl:資源訪問的實現(xiàn)類,其內(nèi)部包含了一個AssetManager,所有資源的訪問都是通過它的Native方法來實現(xiàn)的。
  • ResourcesResourcesImpl的代理類,對于資源的使用者來說,看到的是Resources接口,其實在構(gòu)建Resources對象時,同時也會創(chuàng)建一個ResourcesImpl對象作為它的成員變量,Resources會調(diào)用它來去獲取資源。
  • AssetManager:作為資源獲取的執(zhí)行者,它是ResourcesImpl的內(nèi)部成員變量。

也就是說,資源的訪問最終是由AssetManager來完成,在AssetManager的創(chuàng)建過程中我們首先告訴它資源所在的路徑,之后它就會去以下的幾個地方查看資源,這里面我們看到了第二節(jié)中通過反射調(diào)用的addAssetPath。動態(tài)加載資源的關(guān)鍵,就是如何把包含資源的插件路徑添加到AssetManager當中。


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 在android開發(fā)過程中,我們會用到/res目錄下的文件(圖片,顏色,布局文件等),通過getResource(...
    瘦竹竿閱讀 2,655評論 4 10
  • 1. 概述 我們終于要開始寫插件式換膚框架了,如果一上來就寫或者直接從網(wǎng)上去下載別人寫好的代碼會很坑爹,直接去寫你...
    紅橙Darren閱讀 8,230評論 5 28
  • Android與資源管理相關(guān)的類Resouces和AssetManager很有必要清楚他們的創(chuàng)建過程。 與資源查找...
    小爨閱讀 3,216評論 4 14
  • 插件化-資源處理 寫的比較長,可以選擇跳過前面2節(jié),直接從0x03實例分析開始。如有錯誤,請不吝指正。 0x00 ...
    唐一川閱讀 5,398評論 2 22
  • 其主題是讓高雅音樂走進校園。 剛開始主要是主持人向我們介紹各個樂器的歷史,然后由音樂老師演奏樂器。 但演奏的同時,...
    滑納溪閱讀 294評論 0 3