相關(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
,用于插件資源的訪問。 - 通過插件的路徑,獲得插件的包名信息。
- 通過
resources
的getIdentifier
方法,根據(jù)插件資源的名字以及插件的包名獲取對應(yīng)的資源Id
。 - 通過
resources
的getXXX
方法,傳入前一步中獲取到的資源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)用到ContextWrapper
的getResources()
方法,而該方法又會調(diào)用它內(nèi)部的mBase
變量的對應(yīng)方法:
第二步
mBase
的類型為ContextImpl
,它的getResources()
方法,返回的是其內(nèi)部的成員變量mResources
:
mResources
變量是在ContextImpl
的構(gòu)造函數(shù)中通過下面賦值的:其中
packageInfo
的類型為LoadedApk
,LoadedApk
是對于.apk
解析的結(jié)果,它內(nèi)部包含了所關(guān)聯(lián)的ActivityThread
,安裝后拷貝到的目錄,我們在ContextImpl
中賦值的其實就是它內(nèi)部的mResources
對象:第三步
LoadedApk
中會通過調(diào)用ActivityThread
的getTopLevelResources
方法來為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)用
ResourcesManager
的getResources
方法之后,他就會根據(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
對象,取決于ResourcesKey
的equals
方法是否相等:
-
ResourcesImpl
:資源訪問的實現(xiàn)類,其內(nèi)部包含了一個AssetManager
,所有資源的訪問都是通過它的Native
方法來實現(xiàn)的。 -
Resources
:ResourcesImpl
的代理類,對于資源的使用者來說,看到的是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 知識梳理系列:
- Android 知識梳理目錄:http://www.lxweimin.com/p/fd82d18994ce
- 個人主頁:http://lizejun.cn
- 個人知識總結(jié)目錄:http://lizejun.cn/categories/