動態(tài)加載座泳?哇惠昔,好高大上的名字,一看到是不是就點(diǎn)膽怯了挑势?但如果你肯花點(diǎn)時間瞅一眼舰罚,你就不會這么想了。
因?yàn)樗娴牟荒敲措y薛耻。想想我們經(jīng)常碰到過JAVA中的一個經(jīng)典的設(shè)計(jì)模式:動態(tài)代理营罢。什么?沒聽說過饼齿,年輕人饲漾,你需要補(bǔ)補(bǔ)你的基礎(chǔ)知識了。
動態(tài)代理的原理不就是反射么缕溉?所以考传, 我今天講的這個動態(tài)加載,也是一樣证鸥。
首先僚楞,我們要做什么,要了解插件化的含義是什么枉层?它需要什么必要條件泉褐?簡單形容,你可以把它看成一個可拆卸重裝的活動板房鸟蜡。那么活動板房需要什么必要的存在膜赃?我們得有框架對吧?然后可以移動的門窗揉忘,那么我插件化跳座,就需要一個宿主(框架)端铛,一個插件(門窗)。插件可以隨時拆卸更新而會影響宿主的正常運(yùn)行疲眷。
插件化的目的:隨著應(yīng)用在市場使用禾蚕,業(yè)務(wù)的拓展,功能越來越多狂丝,那么代碼就也會越來越多换淆,需要效果也越來越復(fù)雜,第一存在的問題就是android應(yīng)用的65535的問題(65K)美侦,當(dāng)然你會說google提供了dex分包支持。好吧魂奥,第二個問題菠剩,發(fā)包大小呢?是不是個大問題耻煤。從一開始的幾M到幾十M甚至幾百M(fèi)具壮,你可以忍受,客戶可無法忍受哈蝇。所以我們的插件化是非常有效的解決這些問題棺妓。
好了,啰嗦這么多炮赦,我們來看看具體怎么做:
1怜跑、新建一個宿主程序,下面是主要代碼:
1.1 選擇皮膚(一個未安裝的apk文件):為了簡單吠勘,我直接放在sdcard下面了性芬,實(shí)際上業(yè)務(wù)我們需要放在服務(wù)端然后下載到本地。
private void switchSkin() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "skin.apk";
String pkgName = getSkinApk(this, path);
loadSkinApk(path, pkgName);
}
1.2 根據(jù)apk的路徑獲取包名
private String getSkinApk(Context context, String apkFilePath) {
PackageManager pm = context.getPackageManager();
PackageInfo packageInfo = pm.getPackageArchiveInfo(apkFilePath, PackageManager.GET_ACTIVITIES);
if (packageInfo != null) {
ApplicationInfo appInfo = packageInfo.applicationInfo;
return appInfo.packageName;
}
return "";
}
1.3 加載方法剧防,通過resouces本身的getIdentifier可以植锉。
private void loadSkinApk(String apkFilePath, String apkPackageName) {
Resources resources = createResources(apkFilePath);
Drawable drawable = resources.getDrawable(resources.getIdentifier("skin", "drawable", apkPackageName));
findViewById(R.id.main).setBackground(drawable);
}
我這里還介紹另外一種方法,即DexClassLoader 插隊(duì)方法也可以實(shí)現(xiàn):
private void loadSkinApk(String apkFilePath, String apkPackageName) {
Resources resources = createResources(apkFilePath);
int id = getSkinId(apkFilePath, "skin", apkPackageName);
findViewById(R.id.main).setBackground(resources.getDrawable(id));
}
private int getSkinId(String apkPath, String skinName, String apkPackageName) {
int id = 0;
try {
/**使用DexClassLoader可以加載未安裝的apk中的dex*/
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, this.getDir("dex",
Context.MODE_PRIVATE).getAbsolutePath(), null, this.getClassLoader());
//通過使用apk自己的類加載器峭拘,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源id
Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
Field field = null;
try {
field = clazz.getDeclaredField(skinName);
//得到圖片id
id = field.getInt(R.id.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return id;
}
1.4 最關(guān)鍵的兩個方法
//這個Resources就可以加載非宿主apk中的資源
private Resources createResources(String pFilePath) {
final AssetManager assetManager = createAssetManager(pFilePath);
Resources superRes = this.getResources();
return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
//通過反射的方式拿到assetManager俊庇。
private AssetManager createAssetManager(String pFilePath) {
try {
final AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
assetManager, pFilePath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
2、新建一個插件程序,命名為skin.apk放到測試的手機(jī)sdcard中鸡挠。
3辉饱、效果圖:
最后小提示,記得加權(quán)限<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />