思考
一般情況下,我們在設計一個外掛化框架的時候,要解決的無非是下面幾個問題:
-
四大元件的動態註冊
-
元件相關的類的載入
-
資源的動態載入
實際上從目前的主流外掛化框架來看,都是滿足了以上的特點,當然因為Activity是大家最常用到的,因此一些外掛化框架便只考慮了對Activity的支援,比如Small框架,從原理上來看,基本都差不多,Hook了系統相關的API來接管自己的載入邏輯,特別是Hook 了AMS(ActivityManagerService)以及ClassLoader這2個,因為這2個控制著四大元件的載入以及執行邏輯,這裡的Hook指的是Hook了遠端服務在本地程式的代理物件而已,由於程式隔離的存在,是沒辦法直接Hook遠端程式(Xposed可以Hook掉系統服務,暫時不討論這個),但根據Binder原理,只需要Hook掉遠端程式在本地程式的代理物件即可為我們服務,從而實現我們想要的邏輯,而資源的動態載入僅僅是本地程式的事情,今天我們來簡單討論一下。
動態載入資源例子
下面我們首先通過一個例子來說說,很簡單的例子,就是動態載入圖片,文字和佈局,首先新建一個application的Model,
我們在string.xml加入一個文字,比如:
<resources>
<string name="app_name">ResourcesProject</string>
<string name="dynamic_load">動態載入文字測試</string>
</resources>
複製程式碼
然後弄一個支付寶的圖片用來測試,
然後寫一個佈局activity_text.xml用來動態載入,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:text="動態載入佈局"
android:layout_width="wrap_content"
android:textSize="20sp"
android:layout_height="wrap_content" />
</LinearLayout>
複製程式碼
我們將這個專案打包成一個apk檔案,命名為plugin.apk,打包檔案放在assets目錄下面,最後放到SD卡目錄下面的plugin目錄下面就好,程式碼如下
public static void copyFileToSD(Context context) {
try {
InputStream fis = context.getAssets().open("plugin.apk");
String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
File file = new File(sdPath, "plugin");
if (!file.exists()) {
file.mkdirs();
}
OutputStream bos = new FileOutputStream(file.getAbsolutePath() + File.separator + "plugin.apk");
byte[] buffer = new byte[1024];
int readCount = 0;
while ((readCount = fis.read(buffer)) != -1) {
bos.write(buffer, 0, readCount);
}
bos.flush();
fis.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
當然6.0以上注意一下SD卡許可權就好,
好了,已經把apk檔案放在sd卡了,現在來載入測試一下吧,下面 是程式碼: private void loadPlugResources() {
try {
String resourcePath = Environment.getExternalStorageDirectory().toString() + "/plugin/plugin.apk";
AssetManager mAsset=AssetManager.class.newInstance();
Method method=mAsset.getClass().getDeclaredMethod("addAssetPath",String.class);
method.setAccessible(true);
method.invoke(mAsset,resourcePath);
/**
* 構建外掛的資源Resources物件
*/
Resources pluginResources=new Resources(mAsset,getResources().getDisplayMetrics(),getResources().getConfiguration());
/**
* 根據apk的檔案路徑獲取外掛的包名資訊
*/
PackageInfo packageInfo=getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
//獲取資源的id並載入
int imageId=pluginResources.getIdentifier("alipay","mipmap",packageInfo.packageName);
int strId = pluginResources.getIdentifier("dynamic_load", "string", packageInfo.packageName);
int layoutID = pluginResources.getIdentifier("activity_test", "layout", packageInfo.packageName);
//生成XmlResourceParser
XmlResourceParser xmlResourceParser=pluginResources.getXml(layoutID);
imageView.setImageDrawable(pluginResources.getDrawable(imageId));
textView.setText(pluginResources.getString(strId));
View view= LayoutInflater.from(this).inflate(xmlResourceParser,null);
mView.addView(view,0);
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
我們簡單分析一下上面的流程:
1.首先是根據AssetManager 的原理,呼叫隱藏方法addAssetPath把外部apk檔案塞進一個AssetManager ,然後根據
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
}
複製程式碼
生成一個外掛的Resource物件。
2.根據Resources物件呼叫getIdentifier方法獲取了圖片,文字以及佈局的id,分別設定圖片和文字,再動態載入了一個佈局,呼叫Resources.getXml()方法獲取XmlResourceParser 來解析佈局,最後再載入佈局顯示,執行如圖;
可以看到已經成功載入顯示在介面上了。
動態載入資源原理分析
上面我們看了如何以外掛的形式載入外部的資源,實際上無論是載入外部資源,還是載入宿主本身的資源,它們的原理都是相同的,只要我們弄懂了宿主自身的資源是如何載入的,那麼對於上面的過程自然也就理解了.
在Android中,當我們需要載入一個資源時,一般都會先通過getResources()方法,得到一個Resources物件,再通過它提供的getXXX方法獲取到對應的資源,下面將分析一下具體的呼叫邏輯,首先是當我們呼叫在Activity/Service/Application中呼叫getResources()時,由於它們都繼承於ContextWrapper,該方法就會呼叫到ContextWrapper的getResources()方法,而該方法又會呼叫它內部的mBase變數的對應方法,
@Override
public Resources getResources()
{
return mBase.getResources();
}
複製程式碼
這裡的mBase是一個ContextImpl物件,因為Context是一個抽象類,真正的實現是在ContextIImpl裡面的,它的getResources()方法,返回的是其內部的成員變數mResources,如下程式碼:
@Override
public Resources getResources() {
return mResources;
}
複製程式碼
可見是直接返回了一個mResources物件了,那麼這個mResources是怎麼來的呢,我們可以看到是在ContextImpl的建構函式裡面賦值的,程式碼如下:
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
mMainThread = mainThread;
mActivityToken = activityToken;
mRestricted = restricted;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
? createDisplayWithId
: (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;
CompatibilityInfo compatInfo = null;
if (container != null) {
compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
}
if (compatInfo == null) {
compatInfo = (displayId == Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
}
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setConfiguration(overrideConfiguration);
mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
: ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
//resources 是由packageInfo(LoadedApk )的getResources()方法獲取;
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
//這裡賦值
mResources = resources;
}
複製程式碼
其中packageInfo的型別為LoadedApk,LoadedApk是apk檔案在記憶體中的表示,它內部包含了所關聯的ActivityThread以及四大元件,我們在ContextImpl中賦值的其實就是它內部的mResources物件,程式碼如下: `
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
複製程式碼
可以看到如果為null,那麼返回mainThread.getTopLevelResources方法,這個是主執行緒的方法,如果已經有了,那麼就直接返回mResources物件,我們來看看主執行緒的getTopLevelResources方法:
/**
* Creates the top level resources for the given package.
*/
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, Configuration overrideConfiguration,
LoadedApk pkgInfo) {
return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo());
}
複製程式碼
這裡也是根據安裝的apk的目錄來獲取的,為了更加理解引數,我們來debug一下,如圖:
通過debug,我們可以清楚的看到構造Resource物件所必須的引數的來源,因此,只要具備了這些,就可以任意構造,而不管位置是在哪裡,因此最終呼叫的是mResourcesManager的getTopLevelResources方法,其實裡面也差不多,主要是建立資源,然後快取起來,也是利用了AssetManager原理:
//建立ResourcesKey
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
//判斷快取,如果有快取,直接返回,否則才建立
Resources r;
synchronized (this) {
// Resources is app scale dependent.
if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
//if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
if (r != null && r.getAssets().isUpToDate()) {
if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale
+ " key=" + key + " overrideConfig=" + overrideConfiguration);
return r;
}
}
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
if (splitResDirs != null) {
for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}
// 快取起來
mActiveResources.put(key, new WeakReference<>(r));
複製程式碼
下面我們來分析一下資源的管理者ResourcesManager的一些程式碼:
private static ResourcesManager sResourcesManager;
private final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources =
new ArrayMap<>();
private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
new ArrayMap<>();
CompatibilityInfo mResCompatibilityInfo;
Configuration mResConfiguration;
public static ResourcesManager getInstance() {
synchronized (ResourcesManager.class) {
if (sResourcesManager == null) {
sResourcesManager = new ResourcesManager();
}
return sResourcesManager;
}
}
複製程式碼
我們可以看到是一個單例模式,並且有使用了mActiveResources 作為快取資源物件,sResourcesManager在整個應用程式中只有一個例項的存在,我們上面分析了在建立mResources的時候,是首先判斷是否有快取的,如果有快取了,則直接返回需要的mResources物件,沒有的時候再建立並且存入快取。
ResourcesKey 和ResourcesImpl 以及 Resources 和AssetManager的關係
上面建立資源的程式碼中都出現了他們,那他們到底是什麼關係呢?
●. Resources其實只是一個代理物件,只是暴露給開發者的一個上層介面,我們平時呼叫的getResources().getString(),getgetIdentifier方法等都是給開發者直接用的.對於資源的使用者來說,看到的是Resources介面,其實在構建Resources物件時,同時也會建立一個ResourcesImpl物件作為它的成員變數,Resources會呼叫它來去獲取資源,而ResourcesImpl訪問資源都是通過AssetManager來完成
●. ResourcesKey 是一個快取Resources的Key,也就是說對於一個應用程式,可以儲存不同的Resource,是否返回之前的Resources物件,取決於ResourcesKey的equals方法是否相等
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ResourcesKey)) {
return false;
}
ResourcesKey peer = (ResourcesKey) obj;
if (!Objects.equals(mResDir, peer.mResDir)) {
return false;
}
if (mDisplayId != peer.mDisplayId) {
return false;
}
if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) {
return false;
}
if (mScale != peer.mScale) {
return false;
}
return true;
}
複製程式碼
● ResourcesImpl ,看到命名,我們已經基本明白了是Resources的實現類,其內部包含了一個AssetManager,所有資源的訪問都是通過它的Native方法來實現的
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
mAssets.ensureStringBlocks();
}
複製程式碼
通過建構函式便可以得知mAssets的來源,所有的資源都是通過mAssets訪問的,比如:
int getIdentifier(String name, String defType, String defPackage) {
if (name == null) {
throw new NullPointerException("name is null");
}
try {
return Integer.parseInt(name);
} catch (Exception e) {
// Ignore
}
return mAssets.getResourceIdentifier(name, defType, defPackage);
}
複製程式碼
其他也是類似的。
● AssetManager:作為資源獲取的執行者,它是ResourcesImpl的內部成員變數。
通過上面的分析,我們已經知道了資源的訪問最終是由AssetManager來完成,在AssetManager的建立過程中我們首先告訴它資源所在的路徑,之後它就會去以下的幾個地方檢視資源,通過反射呼叫的addAssetPath。動態載入資源的關鍵,就是如何把包含資源的外掛路徑新增到AssetManager當中
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
複製程式碼
可以看到Java層的AssetManager只是個包裝,真正關於資源處理的所有邏輯,其實都位於native層由C++實現的AssetManager。 執行addAssetPath就是解析這個格式,然後構造出底層資料結構的過程。整個解析資源的呼叫鏈是:
public final int addAssetPath(String path)
=jni=> android_content_AssetManager_addAssetPath
=> AssetManager::addAssetPath => AssetManager::appendPathToResTable => ResTable::add => ResTable::addInternal => ResTable::parsePackage
複製程式碼
解析的細節比較繁瑣,就不細細說明了,有興趣的可以一層層研究下去。
今天的文章就寫到這裡,感謝大家閱讀。