本篇部落格說一下我們的宿主APP怎樣加載别的APK檔案。
首先需要說一些知識點,我們的Java檔案要想在Android環境運作,需要将.java檔案通過轉為class檔案,然後為了能在DVM上面運作,再轉為dex檔案。同理反過來,我們在代碼中要操作的基本都是class檔案,但是class檔案怎麼來呢? 從DexClassLoader加載擷取。
DexClassLoader和PathClassLoader什麼差別呢?
- DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk
- PathClassLoader隻能加載系統中已經安裝過的apk
至于具體源碼差別:建議讀一下DVM源碼。本篇不再贅述,之後專門寫一篇部落格講述DexClassLoader和PathClassLoader的差別。
**
加載外部APK
**
其實這個場景是這樣的:
- 從伺服器下載下傳APK,儲存在我們的手機儲存卡内
- 讀取APK檔案,然後生成對應的DexClassLoader
- 通過DexClassLoader的loadClass方法讀取插件APK dex中的任何一個類。
說幹就幹,首先我們建立一個項目MyPluginProject,在這個項目中建立一個Java類:TestModel
/**
* author: liumengqiang
* Date : 2019/7/27
* Description :
*/
public class TestModel {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
然後打包此項目生成:app-debug.apk。
由于插件APK基本都是從伺服器下載下傳,為了模拟這個場景,我們需要在宿主項目中建立一個assets檔案,将插件APK複制進去,然後在複制到宿主APP的data/data/files檔案夾下。
注意:這裡之前鑽牛角尖了,就是為什麼我不直接手動将插件APK直接複制到data/data/files,檔案夾下呢? 說幹就幹,但是問題來了,我在檔案管理器的目前Android/data/<包名>下找不到此包名,也就是說沒有生成包路徑。 說實話卡了很長時間,我一直以為是不是版本問題,最後求助朋友,折騰了一番,最終手動調用:getExternalCacheDir即可解決。生成路徑是生成了,那麼接下來就是複制APK了吧,當我複制到裡面之後,我發現,尼瑪,死活擷取不到複制進去的插件APK,真的是活見鬼,最後猛然發現那個包路徑是系統路徑!!!這個需要系統簽名權限才能有權限通路! 而我們代碼中assets中的APK是複制到記憶體中。。。
然後在我們的宿主項目中,建立assets,然後将app-debug.apk複制進去。接下來就是将app-debug.apk加載到記憶體中。
/**
* 把Assets裡面得檔案複制到 /data/data/files 目錄下
*
* @param context
* @param sourceName
*/
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
然後在APP啟動的時候調用:
extractAssets(this, "app-debug.apk");
接下來建立DexClassLoader ,加載插件app-debug.apk中的dex:
try {
File extractFile = this.getFileStreamPath("app-debug.apk");
String dexpath = extractFile.getPath();
File fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE
classLoader = new DexClassLoader(dexpath,
fileRelease.getAbsolutePath(), null, getClassLoader());
} catch (Throwable e) {
e.printStackTrace();
}
然後就是我們通過反射擷取TestModel類,并且設定setName,擷取getName:
private void modelTestClick() {
try {
Class<?> loadClass = classLoader.loadClass("com.liumengqiang.mypluginproject.TestModel");
Object newInstance = loadClass.newInstance();
Class[] paramClass = new Class[]{String.class};
Object[] valueObj = new Object[]{"Hello world"};
Method setName = loadClass.getMethod("setName", paramClass);
setName.setAccessible(true);
setName.invoke(newInstance, valueObj);
Method getName = loadClass.getMethod("getName");
getName.setAccessible(true);
String value = (String) getName.invoke(newInstance);
Toast.makeText(this, "Value:" + value, Toast.LENGTH_SHORT).show();
} catch (Throwable e) {
e.printStackTrace();
}
}
至此就調用到了插件中的類。
細看上面的代碼醜得一逼,每次都要通過反射擷取類裡面的方法,如果這個類很多方法,那代碼量可想而知。可以更新一下:面向接口程式設計
**
面向接口程式設計
**
面向接口程式設計是面向對象程式設計的一種實作方式,它的核心思想是将抽象與實作分離,從元件的級别來設計代碼,達到高内聚低耦合的目的。最簡單的面向接口程式設計方法是,先定義底層接口子產品,再定義高層實作子產品。具體的之後在單獨開部落格講述。
上述代碼優化的思路:
- 建立一個interfaceLibrary, 是個Library,裡面定義一個接口:IBaseInterface :
/**
1. author: liumengqiang
2. Date : 2019/8/20
3. Description :
*/
public interface IBaseInterface {
void setName(String name);
String getName();
}
- 然後插件MyPluginProject以及宿主項目分别依賴此Library
- 對于插件MyPluginProject中TestModel類,實作IBaseInterface接口:
/**
* author: liumengqiang
* Date : 2019/7/27
* Description :
*/
public class TestModel implements IBaseInterface {
private String name;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
- 然後打包APK,按照上面的流程。
- 最後在宿主反射擷取TestModel類的時候,就可以通過接口接收建立的TestModel類了。
try {
Class<?> loadClass = classLoader.loadClass("com.liumengqiang.mypluginproject.TestModel");
IBaseInterface newInstance = (IBaseInterface) loadClass.newInstance();
newInstance.setName("hello world");
Toast.makeText(this, "" + newInstance.getName(), Toast.LENGTH_SHORT).show();
} catch (Throwable e) {
e.printStackTrace();
}
是不是精簡了看着。 這是簡單的面向接口程式設計,也就是先定義接口:然後再實作。
Android開發交流:
