本篇博客说一下我们的宿主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开发交流:
