前面已經(jīng)介紹了Android平臺上的幾種ClassLoader椎瘟,這幾種ClassLoader都有各自的使用場景,有了這些基礎(chǔ)知識之后濒持,才能更好地理解以及探究Android熱修復技術(shù)旭旭。首先我們來探究怎么動態(tài)加載外部資源。
1. 動態(tài)加載外部資源
在Android中纹烹,資源文件一般指定義在res資源文件夾中的各種文件,常用到的有字符串資源strings.xml召边、顏色資源colors.xml铺呵、drawable文件等。動態(tài)加載外部資源的目標隧熙,是從一個外部的apk文件中加載資源文件片挂,該apk文件可以是從網(wǎng)絡(luò)下載的,可以是存在于手機存儲目錄中的等等贞盯。
可以想象這樣一種使用場景音念,當你的APP需要具有換膚功能,用戶只需要下載符合你規(guī)范的apk文件(包含皮膚的資源圖片文件等)躏敢,使用動態(tài)加載資源的方式闷愤,加載你下載的apk文件中的資源圖片文件,就能輕松實現(xiàn)換膚功能件余,這樣用戶不需要升級APP讥脐,只需要下載他喜歡的皮膚apk文件就可以了,極大地提高了應用的靈活性啼器。
2. 實現(xiàn)思路
PathClassLoader只能加載手機里已經(jīng)安裝的apk文件旬渠,只有DexClassLoader能加載任意目錄(有讀寫權(quán)限)的apk文件。所以我們考慮先使用DexClassLoader來加載外部的apk文件镀首,再通過該ClassLoader去加載特定的類坟漱,最后通過反射來調(diào)用類里的方法,從而獲取外部資源
3. 實現(xiàn)案例
首先更哄,我們需要有2個工程:一個是宿主工程芋齿,用來加載外部資源;另一個是插件工程成翩,用來提供外部資源觅捆。
3.1 插件工程
我們定義一個字符串資源、一個顏色資源麻敌、一個圖片資源栅炒,然后創(chuàng)建一個類來讀取這些資源。
- 字符串資源定義
<string name="content_plugin">插件APK資源里的文本內(nèi)容</string>
- 顏色資源定義
<color name="color_from_plugin">#66</color>
- 在圖片文件夾里放一個名為test.png的圖片
- 創(chuàng)建讀取資源文件的類及方法
package com.hjy.plugin;
import android.content.Context;
import android.graphics.drawable.Drawable;
public class Utils {
/**
* 直接返回文本字符串
*
* @return
*/
public static String getTextFromPlugin() {
return "插件APK類里的文本內(nèi)容";
}
/**
* 讀取資源文件里的文本字符串
*
* @param context
* @return
*/
public static String getTextFromPluginRes(Context context) {
return context.getResources().getString(R.string.content_plugin);
}
public static Drawable getDrawableFromPlugin(Context context) {
return context.getResources().getDrawable(R.mipmap.test);
}
public static int getColorFromPlugin(Context context) {
return context.getResources().getColor(R.color.color_from_plugin);
}
}
該類提供了幾個靜態(tài)方法,分別來讀取包里的字符串赢赊、顏色乙漓、圖片。
編譯好該插件工程后释移,我們將生成的apk文件命名為plugin-debug.apk叭披,將該apk文件復制到手機SD卡根目錄,可使用命令"adb push plugin-debug.apk /mnt/sdcard/plugin-debug.apk
"玩讳。不一定要放到SD卡根目錄涩蜘,可以是手機上的任何存儲目錄,只要具有讀寫權(quán)限即可熏纯,我這里只是為了演示方便而已同诫,下面都將以該目錄為準。
3.2 宿主工程
我們創(chuàng)建一個宿主工程樟澜,加載插件工程生成的apk文件误窖,并顯示出插件里的資源。
public class MainActivity extends AppCompatActivity {
private Button mBtnTest;
private TextView mTvText1;
private TextView mTvText2;
private ImageView mIvImg;
private DexClassLoader mCustomClassLoader;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnTest = findViewById(R.id.btn_test);
mTvText1 = findViewById(R.id.tv_text1);
mTvText2 = findViewById(R.id.tv_text2);
mIvImg = findViewById(R.id.iv_image);
//優(yōu)化后的dex文件輸出目錄秩贰,應用必須具備讀寫權(quán)限
String optimizedDirectory = getDir("dex", MODE_PRIVATE).getAbsolutePath();
mCustomClassLoader = new DexClassLoader("/mnt/sdcard/plugin-debug.apk", optimizedDirectory, null, getClassLoader());
mBtnTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadResFromPluginApk();
}
});
}
private void loadResFromPluginApk() {
try {
Class clazz = mCustomClassLoader.loadClass("com.hjy.plugin.Utils");
//加載插件里類中定義的字符串資源
Method method = clazz.getMethod("getTextFromPlugin", new Class[]{});
String text = (String) method.invoke(null);
mTvText1.setText(text);
//加載插件里的字符串資源
method = clazz.getMethod("getTextFromPluginRes", Context.class);
text = (String) method.invoke(null, MainActivity.this);
mTvText2.setText(text);
//加載插件里的顏色資源
method = clazz.getMethod("getColorFromPlugin", Context.class);
int color = (int) method.invoke(null, MainActivity.this);
mTvText2.setTextColor(color);
//加載插件里的圖片資源
method = clazz.getMethod("getDrawableFromPlugin", Context.class);
Drawable drawable = (Drawable) method.invoke(null, MainActivity.this);
mIvImg.setImageDrawable(drawable);
} catch (Exception e) {
e.printStackTrace();
}
}
}
代碼很簡單贩猎,就是自己構(gòu)造了一個DexClassLoader對象,通過該ClassLoader去加載插件里Utils類萍膛,然后通過反射調(diào)用Utils類里的各個方法。其中/mnt/sdcard/plugin-debug.apk對應的就是插件apk在手機中的存儲地址嚷堡,根據(jù)實際情況而定蝗罗。
3.3 執(zhí)行效果
我們先運行插件工程,將插件apk傳入手機里面蝌戒。然后再運行宿主工程串塑,點擊測試按鈕開始動態(tài)加載資源。很遺憾的是北苟,這并沒有達到我們的預期效果桩匪,你只會看到第一個TextView有文本顯示,其內(nèi)容為"插件APK類里的文本內(nèi)容"友鼻,第二個TextView顯示的文本并不是插件工程里定義的傻昙,第三個ImageView的內(nèi)容為空,并且控制臺可以看到拋出了android.content.res.Resources$NotFoundException異常彩扔,也就是資源未找到妆档。
3.4 異常分析
從執(zhí)行結(jié)果中可以看到,在宿主工程中反射調(diào)用Utils類的方法時虫碉,只有第一個方法返回成功贾惦,后面幾個方法執(zhí)行都出現(xiàn)異常,到這里是不是有點沮喪了,第一個方法能正確返回內(nèi)容须板,這說明插件apk已經(jīng)被正確的加載了碰镜,但是為什么后面的幾個都失敗了呢?
別急习瑰,我們來看看第一個方法與其他的有什么差別绪颖。第一個方法為getTextFromPlugin(),沒帶任何參數(shù)杰刽,直接返回的是一個固定的字符串菠发,第二個方法為getTextFromPluginRes(Context context),帶有一個參數(shù)Context贺嫂,通過Context去獲取資源滓鸠,由此我們斷定問題是不是就出在這里。
在Android中第喳,apk中的資源都是通過Resources對象來獲取的糜俗,我們在反射調(diào)用后面幾個方法時,Context參數(shù)傳入的是MainActivity.this曲饱,這個是宿主工程的Context悠抹,因此加載插件apk資源用的實際是宿主的Resources對象,但是宿主的Resources對象目前并不能訪問插件apk的資源扩淀,所以會出現(xiàn)資源找不到的異常楔敌。
4. 訪問外部資源的正確姿勢
上面這個例子中可以分析出,從宿主工程中的Context對象獲取到的Resources對象驻谆,無法加載插件apk中的資源文件卵凑,只需要解決該問題,那么我們的動態(tài)加載資源就大功告成了胜臊。
通過Context.getResources()方法勺卢,可以獲取到Resources對象,所以我們需要重寫宿主工程的getResources()方法象对,重新創(chuàng)建一個能讀取插件apk資源的Resources對象黑忱,在宿主工程的MainActivity類中,需要完善的代碼如下:
/**
* 1.重新創(chuàng)建一個AssetManager資源管理器勒魔,通過反射調(diào)用addAssetPath()方法甫煞,可以加載插件apk中的資源。
* <br/>
* 2.依賴第一步創(chuàng)建的AssetManager冠绢,重新創(chuàng)建一個Resources對象危虱,該Resources對象包含了插件apk中的資源。
* <br/>
* 3.插件apk中的資源是通過Context.getResources()來獲取的唐全,因此需要重寫Context的getResources()方法埃跷,返回前面創(chuàng)建的Resources對象蕊玷。
*
* @param dexPath 插件路徑
*/
protected void loadPluginResource(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.invoke(assetManager, dexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources resource = getResources();
mResources = new Resources(mAssetManager, resource.getDisplayMetrics(), resource.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
return mAssetManager != null ? mAssetManager : super.getAssets();
}
@Override
public Resources getResources() {
return mResources != null ? mResources : super.getResources();
}
@Override
public Resources.Theme getTheme() {
return mTheme != null ? mTheme : super.getTheme();
}
在onCreate()中加入初始化代碼:
loadPluginResource("/mnt/sdcard/plugin-debug.apk");
這里的關(guān)鍵代碼是用了AssetManager的addAssetPath()方法,這是一個隱藏的方法弥雹,所以需要采用反射來調(diào)用垃帅。重新運行宿主工程,一切OK剪勿,插件apk中的字符串贸诚、顏色、圖片都能正確加載了厕吉,動態(tài)加載資源到此就初步完成了酱固。
5. 其他問題
5.1 宿主工程能正確加載自己工程里的資源嗎?
答案是否定的,原因是宿主工程MainActivity類中的Resources對象是我們新建的头朱,它只綁定了插件apk中的資源运悲,可以寫段測試代碼試試看:
System.out.println(getString(R.string.app_name));
你會發(fā)現(xiàn)打印出來的是插件apk的app_name,訪問本工程其他的資源文件也會出現(xiàn)異常项钮。到這里是不是很頭疼班眯,本來以為能動態(tài)加載外部apk的資源文件了,結(jié)果發(fā)現(xiàn)本工程的資源文件無法正常加載烁巫,本末倒置了署隘,那怎么解決這個問題呢?既然我們知道資源文件是通過Resources對象來加載亚隙,那我們只需要在插件工程里磁餐,將Context參數(shù)改成Resources,然后在宿主工程反射調(diào)用插件apk的方法時阿弃,只傳入自己構(gòu)造的Resources參數(shù)即可崖媚,完全沒必要重寫宿主工程MainActivity類里的getResources()方法,這樣避免了宿主工程原本的Resources被污染破壞恤浪。
5.2 通過反射獲取插件工程的資源id
我們這個例子中,插件工程的幾個方法是獲取固定的資源文件肴楷,如果有很多資源文件水由,那豈不是要寫很多對應的方法,這顯然不是我們想要的赛蔫,同樣我們可以通過反射來獲取資源的id砂客,這要宿主工程調(diào)用插件工程的方法時,只需要傳入資源名稱即可呵恢。
/**
* 通過資源名反射獲取資源id
*
* @param pkgName 包名
* @param type 資源類型鞠值,如:string, mipmap, drawable等
* @param resName 資源名稱
* @return 資源id
*/
private int getResId(String pkgName, String type, String resName) {
//構(gòu)造R文件內(nèi)部類的類名
String className = pkgName + ".R$" + type;
try {
Class clazz = mCustomClassLoader.loadClass(className);
Field field = clazz.getField(resName);
field.setAccessible(true);
Integer id = (Integer) field.get(null);
return id;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 0;
}
通過反射獲取插件apk里的字符串資源content_plugin,代碼如下:
int resId = getResId("com.hjy.plugin","string", "content_plugin");
System.out.println(mResources.getString(resId));
這樣是不是靈活了很多渗钉。
6. 小結(jié)
本文只是初步探究了怎么去動態(tài)加載外部資源彤恶,但這是管中窺豹钞钙,有很多問題還沒有解決,不過當了解這些之后声离,談到這些話題的時候就不會覺得那么高深莫測了芒炼。