當須要設計一個插件化的框架,首先須要解決的是如下三個問題:緩存
Activity
的動態註冊若是你們有閱讀過前面一系列的文章,那麼對於如何解決前兩個問題應該能夠有一個大概的思路了。不清楚的能夠重點看一下 插件化知識梳理(6) - Small 源碼分析之 Hook 原理 和 插件化知識梳理(8) - 類的動態加載源碼分析。今天這篇,我就來先了解一下在Android
當中資源是如何加載的。bash
爲了讓你們有一個直觀的認識,咱們先不講源碼,而是來看一個簡單的示例,該示例演示瞭如何以插件的形式加載外部資源。app
這裏,咱們須要將所須要的插件資源放在一個.apk
文件中,所以,咱們建立一個新的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/
目錄下,至此,一個包含資源的插件就準備好了。 spa
如今,咱們進入到宿主模塊當中,讀取這三個資源並進行展現。代碼很短,只有下面幾行:插件
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();
}
}
複製代碼
上面的邏輯爲如下幾步:debug
2.1
中所push
進去的resource-debug.apk
所在的路徑。AssetManager
對象,調用它的addAssetPath
方法,該方法的實參爲第一步中的插件路徑。AssetManager
對象做爲構造函數,建立一個訪問該插件資源的代理對象resources
,用於插件資源的訪問。resources
的getIdentifier
方法,根據插件資源的名字以及插件的包名獲取對應的資源Id
。resources
的getXXX
方法,傳入前一步中獲取到的資源Id
,最終獲取資源,並經過控件進行展現。最終的展現結果爲: 設計
在第二節中,咱們用一個簡單的例子,演示瞭如何以插件的形式加載外部的資源,其實,不管是加載外部資源,仍是加載宿主自己的資源,它們的原理都是相同的,只要咱們弄懂了宿主自身的資源是如何加載的,那麼對於上面的過程天然也就理解了。
在Android
中,當咱們須要加載一個資源時,通常都會先經過getResources()
方法,獲得一個Resources
對象,再經過它提供的getXXX
方法獲取到對應的資源,這一過程能夠用下面這張圖來表示:
在上圖中,咱們看到一共通過了四條線路調用到ResourcesManager
,下面,咱們就對這一過程進行分析:
當咱們調用在Activity/Service/Application
中調用getResources()
時,因爲它們都繼承於ContextWrapper
,該方法就會調用到ContextWrapper
的getResources()
方法,而該方法又會調用它內部的mBase
變量的對應方法:
mBase
的類型爲ContextImpl
,它的getResources()
方法,返回的是其內部的成員變量mResources
:
mResources
變量是在
ContextImpl
的構造函數中經過下面賦值的:
packageInfo
的類型爲
LoadedApk
,
LoadedApk
是對於
.apk
解析的結果,它內部包含了所關聯的
ActivityThread
,安裝後拷貝到的目錄,咱們在
ContextImpl
中賦值的其實就是它內部的
mResources
對象:
LoadedApk
中會經過調用ActivityThread
的getTopLevelResources
方法來爲mResources
變量賦值,在調用的時候會傳入LoadedApk
中的一些信息:
mRes
,它是應用安裝時拷貝到
data/app/{package_name}
下的完整路徑,其它的變量,你們能夠參考斷點中的截圖,這裏就很少分析是怎麼得到的了。
通過上面的步驟,最終會調用到ActivityThread
中的getTopResources
方法,在該方法中會經過ResourcesManager
去尋找訪問資源的對應代理對象Resources
Activity
調用
getResources()
方法,到
ResourcesManager
根據應用的信息去查找訪問資源的代理對象的調用過程,下面,咱們就來看一下
ResourcesManager
是如何管理這些
Resources
對象的。
ResourceManager
採用了單例的模式,所以一個進程當中只會有一個對象,它主要負責管理應用程序當中的Resources
對象,在它的內部有如下兩個關鍵的成員變量:
ResourcesManager
的
getResources
方法以後,他就會根據傳入的參數建立一個
ResourcesKey
,並經過該對象中的屬性做爲索引值,首先查找在上面的緩存中是否已經有對應的
Resources
對象了,若是有,那麼就直接返回,不然再建立一個
Resources
對象。
Resources
其實只是一個代理對象,它內部涉及到的類包括:
ResourcesKey
:做爲緩存的Key
,也就是說對於一個應用程序,能夠保存不一樣的Resource
,是否返回以前的Resources
對象,取決於ResourcesKey
的equals
方法是否相等:
ResourcesImpl
:資源訪問的實現類,其內部包含了一個AssetManager
,全部資源的訪問都是經過它的Native
方法來實現的。Resources
:ResourcesImpl
的代理類,對於資源的使用者來講,看到的是Resources
接口,其實在構建Resources
對象時,同時也會建立一個ResourcesImpl
對象做爲它的成員變量,Resources
會調用它來去獲取資源。AssetManager
:做爲資源獲取的執行者,它是ResourcesImpl
的內部成員變量。也就是說,資源的訪問最終是由AssetManager
來完成,在AssetManager
的建立過程當中咱們首先告訴它資源所在的路徑,以後它就會去如下的幾個地方查看資源,這裏面咱們看到了第二節中經過反射調用的addAssetPath
。動態加載資源的關鍵,就是如何把包含資源的插件路徑添加到AssetManager
當中。