前言
資源,是APK包體積過(guò)大的病因之一。插件化技術(shù)將模塊解耦,通過(guò)插件的形式加載。插件化技術(shù)中,每個(gè)插件都能夠作為單獨(dú)的APK獨(dú)立運(yùn)行。宿主啟動(dòng)插件的類,難免要涉及插件類中的資源問(wèn)題。
那么,如何加載插件資源,就成為一個(gè)待解決的問(wèn)題。
原理
參考APK打包流程:Android插件化基礎(chǔ)-APK打包流程
Android工程在打包成apk時(shí),會(huì)使用aapt將工程中的資源名與id在R.java中一一映射起來(lái)。
R.java
public static final int ic_launcher=0x7f060054;
public static final int ic_launcher_background=0x7f060055;
public static final int ic_launcher_foreground=0x7f060056;
public static final int notification_action_background=0x7f060057;
我們每次加載資源時(shí),先要獲取Resources。然后通過(guò):
Drawable drawable = resources.getDrawable(resId);
獲取對(duì)應(yīng)的資源。
因此,我們的核心思路就是:獲取插件的Resources和插件的resId。
實(shí)踐
那么我們?cè)撊绾潍@得插件的Resources呢?
ContextThemeWrapper.java
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
Resources.java是App資源的管理類。
/**
* Create a new Resources object on top of an existing set of assets in an
* AssetManager.
*
* @param assets Previously created AssetManager.
* @param metrics Current display metrics to consider when
* selecting/computing resource values.
* @param config Desired device configuration to consider when
* selecting/computing resource values (optional).
*/
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
通過(guò)注釋我們可以清晰的看到,真正讓Resources與眾不同的是AssetManager。
我們注意到這個(gè)構(gòu)造方法在PackageParser#parseBaseApk方法中調(diào)用。在此我們可以想到,我們是不是可以仿照Apk的安裝過(guò)程,為一個(gè)未安裝的Apk創(chuàng)建一個(gè)Resources呢?
因此,我們繼續(xù)看AssetManager.java:
/**
* Provides access to an application's raw asset files; see {@link Resources}
* for the way most applications will want to retrieve their resource data.
* This class presents a lower-level API that allows you to open and read raw
* files that have been bundled with the application as a simple stream of
* bytes.
*/
public final class AssetManager implements AutoCloseable {
...
}
通過(guò)注釋我們可以看到,這個(gè)類提供了我們?cè)L問(wèn)資源文件的方式。讀一下源碼,可以找到一個(gè)方法:
AssetManager.java
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
return addAssetPathInternal(path, false);
}
通過(guò)注釋我們可以看到,這個(gè)方法提供了包裝ZIP文件的方法。注釋中說(shuō)這不是用于Applications。但我們知道APK文件其實(shí)就是ZIP文件。
看到這里我們的思路就有了。通過(guò)這個(gè)方法,我們將插件APK的path傳入,包裝一個(gè)AssetManager。然后用AssetManager生成Resources,那么這個(gè)Resources就是插件的Resources。雖然插件APK并未安裝,但我們仿照了安裝的流程。
通過(guò)上面的分析,我們能夠得到一個(gè)獲取插件Resources的方法:
/**
* 獲取對(duì)應(yīng)插件的Resource對(duì)象
* @param context 宿主apk的上下文
* @param pluginPath 插件apk的路徑,帶apk名
* @return
*/
public static Resources getPluginResources(Context context, String pluginPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
// 反射調(diào)用方法addAssetPath(String path)
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
// 將插件Apk文件添加進(jìn)AssetManager
addAssetPath.invoke(assetManager, pluginPath);
// 獲取宿主apk的Resources對(duì)象
Resources superRes = context.getResources();
// 獲取插件apk的Resources對(duì)象
Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
return mResources;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
至此我們獲得了插件的Resources,我們通過(guò)獲取資源,如Drawable的方法是:
Drawable drawable = resources.getDrawable(resId);
因此,我們還缺一個(gè)resId,即插件資源在插件R.java中對(duì)應(yīng)的id。
我們可以通過(guò)反射的方式,獲取R.java中的id:
/**
* 加載apk獲得內(nèi)部資源id
*
* @param context 宿主上下文
* @param pluginPath apk路徑
*/
public static int getResId(Context context, String pluginPath, String apkPackageName, String resName) {
try {
//在應(yīng)用安裝目錄下創(chuàng)建一個(gè)名為app_dex文件夾目錄,如果已經(jīng)存在則不創(chuàng)建
File optimizedDirectoryFile = context.getDir("dex", Context.MODE_PRIVATE);
// 構(gòu)建插件的DexClassLoader類加載器,參數(shù):
// 1、包含dex的apk文件或jar文件的路徑,
// 2、apk、jar解壓縮生成dex存儲(chǔ)的目錄,
// 3、本地library庫(kù)目錄,一般為null,
// 4、父ClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
//通過(guò)使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源id
Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
Field field = clazz.getDeclaredField(resName);//得到名為resName的這張圖片字段
return field.getInt(R.id.class);//得到圖片id
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
完成Resources和resId的獲取后,我們就可以獲取插件的資源了:
int resId = getResId(MainActivity.this.getApplication(), PATH, PLUGIN_PACKAGE_NAME, "ic_launcher");
Resources resources = getPluginResources(MainActivity.this.getApplication(), PATH);
Drawable drawable = resources.getDrawable(resId);
mIvTest.setImageDrawable(drawable);
至此,就是插件化加載的資源的基本思路和原理。
總結(jié)
- 明確思路,通過(guò)獲取插件的Resources和resId來(lái)加載資源
- 通過(guò)仿APK解析過(guò)程,獲取插件Resources
- 通過(guò)對(duì)插件的R.java的反射,獲取resId
- 完成加載