前言
插件化开发目前是非常热门的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中加载动态类,之后使用反射获取要调用的方法,最后执行的结果如下。