前言
插件化開發目前是非常熱門的Android技術,它主要通過将不同的業務對象封裝到插件中,這樣不同的業務可以獨立開發和調試,提高項目的開發效率。APK檔案就是常見的插件檔案格式,它包含了Android應用常見的資源和代碼,不過由于插件沒有被安裝到系統中還需要開發者手動實作插件類的動态加載,這裡就是用一個簡單的Demo來測是從APK插件擷取類并執行裡面的代碼邏輯。
準備
可以使用Android Studio建立APK項目,在項目中直接添加java代碼,先增加一個簡單的實作類,我們需要在另外一個Android應用中擷取這個定義的類并且調用它實作的方法。
package com.example.apkresource;
public class HelloWorld {
// 傳回一個字元串
public String getMessage() {
return "Hello World from APK Resouce";
}
// 計算階乘
public int factorial(int x) {
if (x == ) {
return ;
}
if (x < ) {
return -;
}
return x * factorial(x - );
}
}
執行gradle assemble,由于沒有MainActivity會啟動失敗,不過apk檔案還是生成了,将生成的apk檔案拖到Android Studio編輯區域檢視内部的dex檔案裡有Hello這個類的實作。再把生成的apk檔案推送到Android手機的sdcard裡。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwczLcVmds92czlGZvwVP9EUTDZ0aRJkSwk0LcxGbpZ2LcBDM08CXlpXazRnbvZ2LcRlMMVDT2EWNvwFdu9mZvwVP9E0T5VkeaVXOHFmNk1mYwh2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DNzEzM1YTM5ETNyUDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
adb push appres.apk /sdcard/
appres.apk: file pushed. MB/s ( bytes in s)
加載類實作
Android中類加載器有都最終繼承自java.lang.ClassLoader,這裡僅介紹和動态加載相關的類加載器,主要有下面三種。
類加載器 | 注釋 |
---|---|
BootClassLoader | 和java虛拟機中不同的是BootClassLoader是ClassLoader内部類,由java代碼實作而不是c++實作,是Android平台上所有ClassLoader的最終parent,這個内部類是包内可見 |
BaseDexClassLoader | 負責從指定的路徑中加載類,加載類裡面的各種校驗、檢查和初始化工作都由它來完成 |
PathClassLoader | 繼承自BaseDexClassLoader,隻能加載已經安裝到Android系統的APK裡的類,主要邏輯由BaseDexClassLoader實作 |
DexClassLoader | 繼承自BaseDexClassLoader,可以加載使用者自定義的其他路徑裡的類,主要邏輯都由BaseDexClassLoader實作 |
URLClassLoader隻能用于加載jar檔案,由于Android裡的虛拟機隻識别dex檔案,因而在Android中無法使用這個加載器。
對于本Demo中從SDCard裡加載類可以使用DexClassLoader來做加載器,它内部會首先從dexPath路徑出找dex檔案,再用odex優化dex檔案到optimizedDirectory位置,最後再加載優化之後的dex檔案裡的class對象。這個類主要有四個構造參數:
- dexPath:要加載的類所在的jar或者apk檔案路徑,類裝載器将從該路徑中尋找指定的目标類,該類必須是APK或jar的全路徑
- optimizedDirectory:odex優化之後的dex存放路徑,真正的資料是從這個位置的dex檔案加載的,由于ClassLoader隻能加載内部存儲路徑中的dex檔案,是以這個路徑必須為内部路徑
- libPath:目标類中所使用的C/C++庫存放的路徑
- classloader:本裝載器的父裝載器,一般使用目前執行類的裝載器就可以了,在Android用context.getClassLoader()就可以了
private void loadClass() {
// 擷取前面推送到SDCard中的插件路勁
String apkPath = Environment.getExternalStorageDirectory() + File.separator + "appres.apk";
// 優化後的dex存放路徑
String dexOutput = getCacheDir() + File.separator + "DEX";
File file = new File(dexOutput);
if (!file.exists()) file.mkdirs();
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, dexOutput, null, getClassLoader());
try {
// 從優化後的dex檔案中加載APK_HELLO_CLASS_PATH類
clazz = dexClassLoader.loadClass(APK_HELLO_CLASS_PATH);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
調用了上面的加載動态類之後檢視内部緩存下面的DEX檔案夾包含了優化之後的dex檔案。
測試Demo
測試Demo主要是兩個按鈕,一個按鈕負責調用getHello方法,一個負責調用factorial方法計算階乘,最後的結果會存放到界面裡的TextView視圖上。
if (v == getHello) {
if (clazz == null) { // 如果還沒有加載類
loadClass(); // 動态加載類
}
if (clazz != null) {
try {
Object object = clazz.newInstance(); // 建立類執行個體
Method method = clazz.getMethod("getMessage"); // 後去getMessage方法
String text = (String) method.invoke(object); // 調用傳回結果
mText.setText(text); // 将結果設定到text上
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
} else if (v == factorial) {
if (clazz == null) {
loadClass();
}
if (clazz != null) {
try {
Object object = clazz.newInstance(); // 建立類執行個體
Method method = clazz.getMethod("factorial", int.class); // 擷取factorial(int)方法
int value = (int) method.invoke(object, ); // 調用factorial(6)
mText.setText(String.valueOf(value));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
上面的實作首先loadClass從APK中加載動态類,之後使用反射擷取要調用的方法,最後執行的結果如下。