由于项目需要一个jar包,但是这个jar包比较大,有几百kb(项目的优点之一就是安装包体积小),综合考虑后决定采用网络下载后动态加载jar包。于是我用周六日两天研究了一下这个技术,记录下来,以备以后查阅。
关于动态加载,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
DexClassLoader :可以加载文件系统上的jar、dex、apk。jar和apk能被加载也是因为里面包含dex的格式。
PathClassLoader(Android应用中的默认加载器):只能加载/data/app中的apk,也就是已经安装到手机中的apk。
URLClassLoader :可以加载java中的jar,但是由于android不能直接识别jar,所以此方法在android中无法使用。
通过Eclipse生成jar包步骤是,选中工程,右键 ->Export->Java目录下的JAR file -> 选择要打包进去的java文件,选择jar文件的存储路径。-> Finish.
原始的jar是不行的,需要转换成android能识别的字节码dex。使用dx工具,可以在andorid的SKD目录下面搜索一下这个工具,以前的说在platform-tools这个目录下面但是我没有找到,我是在build-tools的子目录里面找到了不同版本的dx工具,我将最新版本的dx文件和当前目录下得lib目录一起拷贝到platform-tools。
转换命令 :
dx --dex --output=dest.jar src.jar
先简单介绍一下DexClassLoader 和 PathClassLoader
public DexClassLoader(String dexPath, String dexOutputDir, String libPath, ClassLoader parent)
public PathClassLoader(String path, String libPath, ClassLoader parent)
dexPath:是加载apk/dex/jar的路径,必须是全路径,比如/data/app/com.ijinshan.browser_fast.apk.如果要包含多个路径(多个jar包),路径之间必须使用特定的分隔符,这个特定分隔符可使用 File.pathSeparator获得
dexOutDir:是dex的输出路径(因为加载apk/jar的时候会解压除dex文件,这个路径就是保存dex文件的),一般写在本程序的数据路径中,比如getApplicationContext().getDir("dex", 0)
libPath:是加载的时候需要用到的c/c++库存放的路径,这个一般不用。
parent:给DexClassLoader指定父加载器,一般为当前执行类的加载器。
可以看出来PathClassLoader的构造函数比DexClassLoader的少了一个参数dexOutDir,
这个原因很简单啦,因为PathClassLoader加载的是安装后的也就是/data/app中得apk,
而这部分的apk都会解压释放dex到指定的目录/data/dalvik-cache,这个释放解压操作
是系统做得,所以不需要dexOutDir参数。
下面就是jar包中的代码,有对象函数也有静态函数和枚举。
package com.dynamic;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.widget.Toast;
public class DynamicTest {
public enum arithmetic
{
plus, minus, multiply, divide;
};
private Context mContext;
public DynamicTest(Context context) {
mContext = context;
}
public static String helloWorld() {
return "helloWorld";
}
public static int arithmetic(arithmetic sign, int a, int b) {
int s = 0;
switch (sign) {
case plus:
s = a + b;
break;
case minus:
s = a - b;
break;
case multiply:
s = a * b;
break;
case divide:
if (b != 0) {
s = a / b;
}
break;
default:
break;
}
return s;
}
public void toastShow(String showString) {
Toast.makeText(mContext, showString, Toast.LENGTH_SHORT).show();
}
public void showPluginWindow(String[] message) {
AlertDialog.Builder builder = new Builder(mContext);
builder.setMessage(message[1]);
builder.setTitle(message[0]);
builder.setNegativeButton(message[2], new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
Dialog dialog = builder.create();
dialog.show();
}
}
右键工程选择Export
选择要导出的文件保存成jar包,然后在终端执行 cd 到platform-tools目录下执行
dx --dex --output=test.jar dynamic.jar
将转码后的test.jar push到手机的sd卡上
adb push test.jar /sdcard/text.jar
然后就是通过反射执行这些函数,下面是对4个函数的调用
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory()
.toString() + File.separator + apkFileName);
optimizedDexOutputPath.setExecutable(true);
File dexOutputDirs = getApplicationContext().getDir("dex", 0);
final DexClassLoader cl = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(),
dexOutputDirs.getAbsolutePath(), null, getClass().getClassLoader());
mToastButton = (Button) findViewById(R.id.helloWorld);
mArithmetic = (Button) findViewById(R.id.arithmetic);
mToastShow = (Button) findViewById(R.id.toastShow);
mShowPluginWindow = (Button) findViewById(R.id.showPluginWindow);
mShowPluginWindow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Class<?> libProviderClazz = null;
try {
libProviderClazz = cl.loadClass("com.dynamic.DynamicTest");
String[] strMessage = {
"title", "message", "ok"
};
Method method = libProviderClazz.getDeclaredMethod("showPluginWindow",
new Class[] {
String[].class
});
Constructor<?> con = libProviderClazz.getConstructor(Context.class);// 返回该类的构造方法
method.invoke(con.newInstance(MainActivity.this), (Object) strMessage);
} catch (Exception exception) {
exception.printStackTrace();
}
}
});
mToastShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Class<?> libProviderClazz = null;
try {
libProviderClazz = cl.loadClass("com.dynamic.DynamicTest");
Method method = libProviderClazz.getDeclaredMethod("toastShow", new Class[] {
String.class
});
Constructor<?> con = libProviderClazz.getConstructor(Context.class);// 返回该类的构造方法
method.invoke(con.newInstance(MainActivity.this), "好吗!");
} catch (Exception exception) {
exception.printStackTrace();
}
}
});
mArithmetic.setOnClickListener(new View.OnClickListener() {
@TargetApi(Build.VERSION_CODES.CUPCAKE)
@SuppressLint("NewApi")
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Class<?> libProviderClazz = null;
try {
libProviderClazz = cl.loadClass("com.dynamic.DynamicTest");
Class<?> libProviderEnumClazz = cl
.loadClass("com.dynamic.DynamicTest$arithmetic");
Method[] methods = libProviderEnumClazz.getMethods();
Field[] fields = libProviderEnumClazz.getFields();
for (Method m : methods) { // 打印所有方法
Log.d("abcd", m.toString());
}
for (Field f : fields) {// 打印所有对象
Log.d("abcde", f.toString());
}
Field f = libProviderEnumClazz.getField("multiply");
Method method = libProviderClazz.getDeclaredMethod("arithmetic", new Class[] {
libProviderEnumClazz, int.class, int.class
});
int s = (Integer) method.invoke(null, f.get(libProviderEnumClazz), 5, 8);
Toast.makeText(MainActivity.this, String.valueOf(s), Toast.LENGTH_SHORT).show();
} catch (Exception exception) {
exception.printStackTrace();
}
}
});
mToastButton.setOnClickListener(new View.OnClickListener() {
@SuppressLint("NewApi")
@TargetApi(Build.VERSION_CODES.CUPCAKE)
public void onClick(View view) {
Class<?> libProviderClazz = null;
try {
libProviderClazz = cl.loadClass("com.dynamic.DynamicTest");
Method[] methods = libProviderClazz.getMethods();
for (Method m : methods) {
Log.d("abcd", m.toString());
}
Method method = libProviderClazz
.getDeclaredMethod("helloWorld", new Class[] {});
// 如果传递给Method对象的invoke()方法的第一个参数为null,说明该Method对象对应的是一个静态方法,不通过对象调用
Toast.makeText(MainActivity.this, (String) method.invoke(null),
Toast.LENGTH_SHORT).show();
} catch (Exception exception) {
exception.printStackTrace();
}
}
});
}
通过ClassLoader装载类,调用其内部函数的过程有点烦琐,包括先构造出Method对象,并构造出Method对象所使用的参数对象,然后才能调用。
有一种方法比较简单,那就是定义interface接口,interface仅仅定义函数的输入输出,却不定义函数的具体实现,然后在插件工程导出jar包的时候不要勾选interface文件,因为interface是要包含在主工程里的,如果导出jar包也被包含在里面,程序运行的时候是会出错的(程序运行时包含了两份interface文件)。因为不像反射这么烦琐,就不记录了。