https://blog.youkuaiyun.com/lin20044140410/article/details/104584877
什么是插件化?
直白点说,就是去运行没有安装的apk,把这些没有安装过的apk理解为插件,把运行这些插件的apk称为宿主.
为什么需要插件化?
由于宿主可以在运行时动态去加载和运行插件,这就可以把apk中不常用的功能模块做成插件,减小了apk包的大小,实现了apk功能的动态扩展.
插件化和组件化的区别:
组件化,是把app分成多个模块,独立开发,根据需求,利用gradle配置模块间的依赖关系,但是最终发布时模块和app会打包成一个apk.
插件化,也是把app分成多个模块,这些模块中有一个宿主,多个插件,最终打包时宿主apk和插件apk可以分开打包,插件apk也可以在需要时由网络下载,然后加载运行.
插件化实现的思路:
1)加载插件的类
2)加载插件的资源
3)启动插件中的组件(四大组件)
这篇文章先来实现第一个问题:加载插件的类,这里插件的类,是普通的类,不是四大组件,四大组件的加载运行会放在后面的文章来讨论.
加载类就要用到类加载器,java中jvm加载的class文件,android中的虚拟机加载的dex文件(就是多个class文件的合并),这里主要看android中如何加载dex文件的.
android中的类加载分两种:系统类加载器,自定义的类加载器,他们的关系如图:
BootClassLoader 用于加载framework层的class文件
PathClassLoader 用于加载应用程序中class文件,这里也包括应用中依赖的库文件,具体形式可以是apk,jar,zip中的dex文件.
DexClassLoader 用于加载制定的dex文件.
在一个自动生成的app中,MainActivity.java 这个类是由PathClassLoader来加载的, 而android.app.Activity.java这个framework中类时由BootClassLoader加载的.
从源码看,DexClassLoader,PathClassLoader除了构造函数外,没有实现任何代码,类加载的工作都是由父类BaseDexClassLoader时完成的.
这里有一点需要注意的是:PathCalssLoader的parent类加载器是BootClassLoader,并不是其继承关系的父类.
在分析类加载流程之前,先看下类的生命周期:
加载 是 类加载过程中的一个阶段,在加载阶段,虚拟机要完成3件事情:
1)通过类的全限定名,获取类的二进制字节流
2)将这个字节流代表的静态存储结构,转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口,这个Class对象是比较特殊的,虽然它是对象,却是放在方法区的.
下面就通过源码看下加载一个类的过程,从loadClass方法的实现看起,DexClassLoader,PathClassLoader及父类BaseDexClassLoader,一直往上查找,这个方法的实现在ClassLoader.java中.
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
这里的实现就是双亲委派模型,
1,首先去查找这个类是不是已经加载过,是就直接返回,否则,
2, 递归的去请求parent加载器去完成加载,在递归过程中,走到BootClassLoader后,就不会再请求其parent来加载,而是通过其findClass来加载,如果无法完成这个加载请求,就返回null.
3,如果都没有加载成功,才由自己来完成加载findClass,
这个双亲委托机制,让类的加载有了一种优先级的层次关系,比如我们Activity这个类,不管是谁请求加载这个类,最终都是委托给这个模型最顶端的BootClassLoader来加载,所以activity在各种类加载环境中都是同一个类,如果没有这种机制,自己去定义类加载器,然后自己定义一个android.app.Activity类,那么系统中就会出现多个不同的Activity类,那就乱了.
从上面双亲委派模型的加载流程,如果要自己加载类,时调用findClass方法,下面看BaseDexClassLoader.java中的这个方法的实现:
(因为DexClassLoader,PathClassLoader都没定义这个方法,所以实际调用的是BaseDexClassLoader.java中的)
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
它又是通过DexPathList来实现的.
DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
dexElements是一个数组,其中的元素 Element里面包含了dex文件,如果app中有多个dex文件,这个数组就会有多个Element元素,每个Element元素中包含一个dex文件,
DexPathList#Element.java
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
最终是通过Element中的dexfile来获取类的二进制字节流.
在回头去看看dexElements这个数组怎么来的?
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
从构造函数的回溯,可以看到dexElements中元素,来自与第一个参数dexpath,这个路径通常就是apk的路径,或者jar,zip,dex文件的路径,具体的值是有创建类加载器传入的参数确定.
分析到这里,应该能明白一点:如果要让app能加载插件的class文件,只要把插件的dex文件,也存入dexElement数组就OK 了.
通过代码验证,使用DexClassLoader,PathClassLoader能不能成功加载插件的类.
先定义一个插件module,新建测试类:
package com.test.plugintest;
import android.util.Log;
public class PlugInDemo {
public static void testPlugIn() {
Log.d("PlugIn","from plugin PlugInDemo.");
}
}
把这个类打包成一个dex文件,具体做法是使用sdk下dx工具,把class文件打包成dex,
D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>
dx --dex --output=plugIn.dex com\test\plugintest\PlugInDemo.class
D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>
执行上面的命令,会在当前目录下生成一个plugIn.dex文件,这里有一点值得注意,执行dx命令的位置最好在classes这个目录,然后指定具体class文件时,使用全类名
把plugIn.dex拷贝到sdcard目录下,
public void loadClassPlugIn() {
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
MainActivity.this.getCacheDir().getAbsolutePath(),
null,
MainActivity.this.getClassLoader());
// PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
// MainActivity.this.getClassLoader());
try {
Class<?> clazz = dexClassLoader.loadClass("com.test.plugintest.PlugInDemo");
Method method = clazz.getMethod("testPlugIn");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
在宿主App的MainActivity中,可以通过上面的ClassLoader加载PlugInDemo.java类,通过反射调用其中方法。
通过这个测试,只是去验证通过自定义类加载器可以去加载执行一个插件中的类.但是这个插件的类并没有合并到app中.
下面就来实现把插件的类,合并到宿主app中,实现步骤:
1,创建插件的DexClassLoader类加载器,然后反射拿到插件的dexElements数组
2,获取宿主的PathClassLoader类加载器,然后反射拿到宿主的dexElements数组
3,把宿主和插件的两个dexElements数组合并
DexPathList.java
private Element[] dexElements;
BaseDexClassLoader.java
private final DexPathList pathList;
dexElements在DexPathList中,而DexPathList在BaseDexClassLoader中,所以先获取类加载器,然后反射获取到DexPathList,在反射拿到dexElements.
public void loadPlugInModule() {
try {
//BastDexClassLoader,DexPathList,宿主和插件可公用
Class<?> baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoader.getDeclaredField("pathList");
pathListField.setAccessible(true);
Class<?> dexPathListClassLoader = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClassLoader.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//创建插件的DexClassLoader类加载器,然后反射拿到插件的dexElements数组
DexClassLoader plugInDexClassLoader = new DexClassLoader(
"/sdcard/plugintest-debug.apk",
mContext.getCacheDir().getAbsolutePath(),
null,
mContext.getClassLoader());
//获取插件类加载器中DexPathList实例对象
Object plugInDexPathList = pathListField.get(plugInDexClassLoader);
//拿到插件的dexElements数组
Object[] plugInDexElements = (Object[]) dexElementsField.get(plugInDexPathList);
//获取宿主的PathClassLoader类加载器,然后反射拿到宿主的dexElements数组
PathClassLoader hostPathClassLoader = (PathClassLoader) mContext.getClassLoader();
//获取宿主类加载器中DexPathList实例对象
Object hostDexPathList = pathListField.get(hostPathClassLoader);
//拿到宿主的dexElements数组
Object[] hostDexElements = (Object[]) dexElementsField.get(hostDexPathList);
//创建新的DexElements数组
Object[] dexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
hostDexElements.length + plugInDexElements.length);
//合并宿主和插件的dexElements数组
System.arraycopy(hostDexElements,0, dexElements, 0, hostDexElements.length);
System.arraycopy(plugInDexElements, 0, dexElements, hostDexElements.length, plugInDexElements.length);
//用合并后的数组替换宿主的原dexElements数组
dexElementsField.set(hostDexPathList, dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
在将插件apk中class加载后,就可以通过反射访问其中的class属性了.
public void loadClassPlugIn() {
// DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
// MainActivity.this.getCacheDir().getAbsolutePath(),
// null,
// MainActivity.this.getClassLoader());
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
MainActivity.this.getClassLoader());
try {
Class<?> clazz = Class.forName("com.test.plugintest.PlugInDemo");
Method method = clazz.getMethod("testPlugIn");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}