Android-插件化详解
目录
一,定义
二,原理
三,双亲委托机制
四,源码分析
五,插件化原理分析
六,代码实现
一,定义
什么是插件化?插件化就是将整个app拆分成多个模块,这些模块包括一个宿主和多个插件,每一个模块都是一个apk,最终打包的时候宿主apk和插件apk分开打包。
那么这样做的好处是什么呢?
1.减小app的体积,按需下载,用不到某些模块就不需要下载相应的插件apk
2.减小模块之间的耦合度,降低协同开发的成本
3.解决了应用之前的相互调用问题
目前比较流行的插件化框架及其特性如下:
二,原理
首先我们来看一下一个apk文件里面都包含了什么:
可以看到,主要是包含了dex文件和res资源文件等。
我们再看看dex文件里面有什么:
dex文件里面包含了我们所有的类生成的class文件。
所以说,如果我们要加载插件apk中的某个类的某个方法,只需要加载dex文件,通过反射拿到class对象,就可以调用该方法了。
关于反射可以参考文章Java反射机制详解:Class、构造器、成员变量与方法
那么如何加载dex文件呢? 这就涉及到了android的类加载机制。
三,双亲委托机制
android的类加载器有三种,BootClassLoader,PathClassLoader,DexClassLoader.
其中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader,下面我们来看看他们的源码:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
可以看到,他们的区别就是第二个参数 optimizedDirectory,这个参数的意 思是生成的 odex(优化的dex)存放的路径。在8.0(API 26)及之后,二者就完全一样了。
这里需要注意的是,BootClassLoader主要是用来加载SDK的类的,比如android SDK下面的系统的类。而PathClassLoader主要是用来加载应用的类,比如我们手写的类或者应用的第三方库的类(Glide等)。
我们来看看他们三个类之间的关系:
下面我们看一下ClassLoader的loadClass方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
Class<?> c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (this.parent != null) {
c = this.parent.loadClass(name, false);
} else {
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}
if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
this.resolveClass(c);
}
return c;
}
}
首先检测这个类是否已经被加载了,如果已经加载 了,直接获取并返回。如果没有被加载,parent 不 为 null,则调用parent的loadClass进行加载,依次递 归,如果找到了或者加载了就返回,如果即没找到 也加载不了,才自己去加载。
上面代码可以总结为如下的流程:
需要注意的一点是这里的parent并不是父类的意思,而是类似于链表的结构,parent类似于链表的下一个元素,而BootClassLoader类似于链表的末端,所以它没有parent。
这样做的好处是什么呢?
1, 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
2,安全性考虑,防止核心API库被随意篡改。
四,源码分析
这里主要通过源码的角度来分析下插件化的原理,所以只分析findClass部分源码
先看一下BaseDexClassLoader的findClass方法:
@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;
}
然后就走到了pathList的findClass方法,看一下这里的pathList是什么:
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);
}
原来是DexPathList,那接下来就看看DexPathList的findClass方法:
private Element[] dexElements;
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;
}
注意一下这里的Element[]为什么要用一个数组呢,因为我们的apk有时候不止有一个dex文件,可能会有两个或者多个,所以这里采用了数组。
然后接着往下看element.findClass():
private final DexFile dexFile;
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
然后就到了DexFile 的findClass方法,下面就是native方法了。
五,插件化原理分析
通过上面源码我们知道,要想加载插件apk的clas文件,最关键的就是通过dexElements这个数组。正常加载自身apk的话,就是直接获取到自身的dexElements,然后加载class就可以了。
但是如果要加载其他apk的class,那么就必须将这个dexElements替换为同时拥有自身dex文件和插件apk的dex文件的数组。且自身的dex文件必须在前面,如果在后面,那就是热修复了。
所以大致的步骤为:
1. 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
2. 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
3. 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
4. 最后通过反射将新的 Element[] 赋值给宿主的 dexElements 。
原来的:
插件化的:
六,代码实现
1, 获取BaseDexClassLoader类的成员变量pathList
Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clazz.getDeclaredField("pathList");
pathListField.setAccessible(true);
2,获取自身apk的类加载器
ClassLoader pathClassLoader = context.getClassLoader();
3,获取DexPathList类的对象
Object hostPathList = pathListField.get(pathClassLoader);
4,获取DexPathList的类的成员变量dexElements
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
5,获取宿主apk的dexElements对象
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
6,我们把插件的apk随便放在sdcard/data/ 下面,然后获取插件的类加载器
ClassLoader dexClassLoader = new DexClassLoader("/sdcard/data/yztest.apk",context.getCacheDir().getAbsolutePath(),
null, pathClassLoader);
7,获取DexPathList类的对象
Object pluginPathList = pathListField.get(dexClassLoader);
8,获取插件的dexElements对象
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
9,创建一个新的数组
Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
hostDexElements.length + pluginDexElements.length);
10,将宿主apk的dexElements添加到新数组中
System.arraycopy(hostDexElements, 0, newDexElements,
0, hostDexElements.length);
11,将插件apk的dexElements添加到新数组中
System.arraycopy(pluginDexElements, 0, newDexElements,
hostDexElements.length, pluginDexElements.length);
12,将新数组赋值到宿主的dexElements中
dexElementsField.set(hostPathList, newDexElements);
我们的插件apk的测试类如下:
public class YZTest {
public static void testPlugin() {
System.out.prientln("调用了testPlugin方法");
}
}
宿主apk可以直接通过反射调用如下:
try {
Class<?> clazz = Class.forName("com.yuanzhen.plugin.YZTest");
Method print = clazz.getMethod("testPlugin");
print.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}