概述
前段時間在開發(fā)外設驅(qū)動程序時,涉及到了動態(tài)加載jar包的知識,于是開始學習了下。
這里主要是完成客戶端驅(qū)動jar包代碼編寫,所以只涉及到java接口實現(xiàn)問題,然后將jar包經(jīng)過dex轉(zhuǎn)換,放到測試程序的指定目錄下即可進行驅(qū)動測試。
將jar包轉(zhuǎn)換成dex編碼格式
Android中Dalvik/ART虛擬機能夠直接加載的是dex文件。
使用dx命令將導出的驅(qū)動jar包轉(zhuǎn)為dex編碼格式,擴展名還是保持為jar不變即可。
// dx -- 如27.0.3版本為Android\Sdk\build-tools\27.0.3下的dx.bat腳本
// XXX.jar -- 為轉(zhuǎn)換后的文件名,仍保持擴展名為jar
// XXX-origin.jar -- 要進行轉(zhuǎn)換前的jar包
dx --dex --output=XXX.jar XXX-origin.jar
加載經(jīng)過dex轉(zhuǎn)碼后的jar包
Android中使用DexClassLoader來動態(tài)加載指定路徑下的經(jīng)過dex轉(zhuǎn)化過的jar包或apk文件。
DexClassLoader類只有一個構造方法參數(shù)說明
// dexPath -- 經(jīng)dex優(yōu)化的jar包/apk存放路徑
// optimizedDirectory -- 解壓出的dex文件的存放路徑,不可存放在外置存儲,以免被注入攻擊
// librarySearchPath -- C/C++依賴的本地庫文件目錄,可以為null
// parent -- 父類的類加載器
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent);
獲取DexClassLoader實例
private DexClassLoader getClassLoader(String paramString) {
String dexPath = new File(getAppJarDir(), paramString).getAbsolutePath(); // 經(jīng)過dex轉(zhuǎn)碼后的jar包存放路徑
String optimizedDirectory = getAppDexDir().getAbsolutePath(); // dex優(yōu)化文件存放路徑
String librarySearchPath = getAppLibDir().getAbsolutePath(); // 本地依賴庫存放路徑
ClassLoader parent = getClassLoader(); // 父類的類加載器
return new DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent);
}
調(diào)用dex里面的代碼
這里可以使用兩種方式來調(diào)用dex里面的代碼:
- 通過接口實現(xiàn)方式
- 通過反射機制方式
通過接口實現(xiàn)方式
將方法抽象成公共接口,并制作成jar包,直接用于開發(fā)驅(qū)動jar包和驅(qū)動測試程序。
測試程序中可以直接對加載后的類實例進行強制類型轉(zhuǎn)換來直接調(diào)用方法。
接口定義如下:
// (Java Library)
// pluginif/src/main/java/com.shellever.plugin/PluginIf.java
package com.shellever.plugin;
public interface PluginIf {
String getDeviceInfo();
}
接口實現(xiàn)如下:
// (Java Library)
// pluginimpl/src/main/java/com.shellever.plugin/PluginImpl.java
package com.shellever.plugin;
public class PluginImpl implements PluginIf {
@Override
public String getDeviceInfo() {
return "Shellever";
}
}
測試程序中動態(tài)調(diào)用方式如下:
// (Phone)
// app/src/main/java/com.shellever.dexclassloader.MainActivity.java
public void testDexClassLoaderWithIf() {
// 加載接口具體實現(xiàn)的經(jīng)過dex轉(zhuǎn)換過的jar包
DexClassLoader dexClassLoader = getClassLoader("pluginImpl.jar");
try {
// 加載接口具體實現(xiàn)類
Class pluginImplClazz = dexClassLoader.loadClass("com.shellever.plugin.PluginImpl");
PluginIf pluginIf = (PluginIf) pluginImplClazz.newInstance(); // 直接強制類型轉(zhuǎn)換
String devInfo = pluginIf.getDeviceInfo(); // 調(diào)用接口方法
mLoaderResultTv.setText(devInfo);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
通過反射機制方式
通過文檔接口約定來編寫驅(qū)動時,在測試程序中只能通過反射的方法來調(diào)用接口方法了。
驅(qū)動實現(xiàn)代碼如下:
// (Phone)
// app/src/main/java/com.shellever.dexclassloader.MainActivity.java
package com.shellever.plugin.common;
import android.content.Context;
public class Common {
private Context mContext;
public Common(Context context){
mContext = context;
}
public String getDeviceInfo(){
return "Shellever.HPF";
}
}
測試程序這邊考慮到多模塊驅(qū)動時,保證接口的正確性和程序的可擴展性,通過xml文件對模塊接口進行定義:
// app/src/main/res/xml/device_class_module.xml
<?xml version="1.0" encoding="utf-8"?>
<devices>
<modules>
<module id="COMMON" name="com.shellever.plugin.common.Common" args="Context"/>
</modules>
<functions>
<function id="COMMON" name="getDeviceInfo" args=""/>
</functions>
</devices>
其中的標簽及屬性定義說明:
modules用于定義模塊集合;
module用于模塊類路徑及構造方法的定義,id為模塊名稱,name為模塊類全路徑,args為構造方法參數(shù)類型。
functions為具體模塊的方法聲明;
function用于具體方法聲明,id為指定所屬模塊,name為方法名稱,args為方法參數(shù)類型。
測試程序中xml解析代碼如下:
public HashMap<String, DeviceInfo> parseDeviceInfo(){
HashMap<String, DeviceInfo> deviceInfo = new HashMap<>();
XmlResourceParser parser = getResources().getXml(R.xml.device_class_module);
try {
int eventType = parser.getEventType();
while(eventType != XmlPullParser.END_DOCUMENT){
switch (eventType){
case XmlPullParser.START_TAG:
if("module".equals(parser.getName())){
DeviceInfo localDeviceInfo = new DeviceInfo();
localDeviceInfo.name = parser.getAttributeValue(null, "name");
localDeviceInfo.args = parser.getAttributeValue(null, "args");
deviceInfo.put(parser.getAttributeValue(null, "id"), localDeviceInfo);
}
if("function".equals(parser.getName())){
DeviceInfo localDeviceInfo = new DeviceInfo();
localDeviceInfo.name = parser.getAttributeValue(null, "name");
localDeviceInfo.args = parser.getAttributeValue(null, "args");
deviceInfo.put(parser.getAttributeValue(null, "id") + localDeviceInfo.name, localDeviceInfo);
}
break;
}
eventType = parser.next();
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return deviceInfo;
}
public class DeviceInfo {
public String args;
public String name;
}
測試程序中動態(tài)調(diào)用代碼如下:
public void testDexClassLoaderWithReflect() {
HashMap<String, DeviceInfo> deviceInfo = parseDeviceInfo(); // 解析xml定義文件
// 加載接口具體實現(xiàn)的經(jīng)過dex轉(zhuǎn)換過的jar包
DexClassLoader dexClassLoader = getClassLoader("pluginDevInfo.jar");
// 獲取COMMON模塊構造方法及參數(shù)
DeviceInfo localDeviceInfo = deviceInfo.get("COMMON");
String className = localDeviceInfo.name;
String classArgs = localDeviceInfo.args;
Log.d(TAG, "className = " + className);
// 獲取COMMON模塊接口方法及參數(shù)
DeviceInfo localDeviceInfo2 = deviceInfo.get("COMMON" + "getDeviceInfo");
String functionName = localDeviceInfo2.name;
String functionArgs = localDeviceInfo2.args;
Log.d(TAG, "functionName = " + functionName);
try {
Class pluginDevInfoClazz = dexClassLoader.loadClass(className);
Object localObject = pluginDevInfoClazz.getConstructor(getParamType(classArgs)).newInstance(new Object[]{this});
Method localMethod = localObject.getClass().getDeclaredMethod(functionName, getParamType(functionArgs));
String returnType = localMethod.getReturnType().getSimpleName(); // 獲取方法返回值類型
if("String".equals(returnType)){
// 通過反射調(diào)用接口方法
String devinfo = (String) localMethod.invoke(localObject, new Object[0]);
mLoaderResultTv.append("\n" + devinfo);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
導出jar包方式
Eclipse導出jar包比較簡單,詳細步驟可以參考這里: 使用eclipse工程導出jar包
Android Studio中創(chuàng)建Java Library (Android Library會同時創(chuàng)建資源文件及目錄)默認會創(chuàng)建jar任務,直接雙擊運行即可生成jar包。

當創(chuàng)建模塊是app應用程序時,則需要手動編寫Gradle任務來將編譯生成的class文件打包成jar包:
// plugindevinfo模塊打jar包任務
// Phone
task clearJar(type: Delete) {
// 編譯完成后jar包存放位置
delete 'build/libs/pluginDevInfo.jar'
delete 'libs/pluginDevInfo.jar'
}
//打包任務
task makeJar(type: Jar) {
//指定生成的jar名
baseName 'pluginDevInfo'
//從哪里打包class文件
from('build/intermediates/classes/release/com/shellever/plugin')
//打包到jar后的目錄結構
into('com/shellever/plugin/')
//去掉不需要打包的目錄和文件
exclude('BuildConfig.class', 'R.class')
//去掉R$開頭的文件
exclude { it.name.startsWith('R$') }
}
makeJar.dependsOn(clearJar, build)
GitHub源代碼參考
參考文章
使用eclipse工程導出jar包
AS生成jar包,并在其他工程中引用jar包的方法
Android動態(tài)加載入門 簡單加載模式