手写hook式插件化框架
之前介绍了占位式插件化原理:即将插件的context环境替换成宿主的context环境就能加载对应的class和资源。但在使用是有诸多限制,比如插件的页面跳转和资源文件使用时都要特别注意使用宿主的context环境,否则就会出错。而hook式插件话就不需要如此麻烦,要分析其原理,我们需先了解Android进程运行的一些知识。
一.基本原理
1.类加载
外部apk中类的加载
在Android开发中,不管是插件化还是组件化,都是基于Android系统的类加载器ClassLoader来设计的。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并、优化,然后再生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,在早期的Android应用开发中,如果不对Android应用进行分dex处理,那么最后一个应用的apk只会有一个dex文件。
常用的类加载器有两种,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件。而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。
所以我们可以用DexClassLoader去加载外部的apk,用法如下
//第一个参数为apk的文件目录//第二个参数为内部存储目录//第三个为库文件的存储目录//第四个参数为父加载器new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)
双亲委托机制
ClassLoader调用loadClass方法加载类
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//首先从已经加载的类中查找
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
//如果没有加载过,先调用父加载器的loadClass
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
//父加载器都没有加载,则尝试加载
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
可以看出ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载。
DexClassLoader的DexPathList
DexClassLoader重载了findClass方法,在加载类时会调用其内部的DexPathList去加载。DexPathList是在构造DexClassLoader时生成的,其内部包含了DexFile。
DexPathList的loadClass会去遍历DexFile直到找到需要加载的类
有一种热修复技术正是利用了DexClassLoader的加载机制,将需要替换的类添加到dexElements的前面,这样系统会使用先找到的修复过的类。
2.2 单DexClassLoader与多DexClassLoader
通过给插件apk生成相应的DexClassLoader便可以访问其中的类,这边又有两种处理方式,有单DexClassLoader和多DexClassLoader两种结构。
多DexClassLoader
对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题。
单DexClassLoader
将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。
互相调用
插件和主工程的互相调用涉及到以下两个问题
插件调用主工程
- 在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。
主工程调用插件 - 若使用多ClassLoader机制,主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。
- 若使用单ClassLoader机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。
二.代码演示(单ClassLoader机制)
public class HookApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
plugToApplication();
}
private void plugToApplication() {
try {
//第一步,找到宿主的dexElements
PathClassLoader classLoader = (PathClassLoader) getClassLoader();
Class baseClass = Class.forName("dalvik.system.PathClassLoader");
Field pathListField = baseClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object mDexPathList = pathListField.get(classLoader);
Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElements = dexElementsField.get(mDexPathList);
//第二步,找到插件的dexElements。需注意插件的classLoader需要新构建,因为路径已经变化了
File file = new File(Environment.getExternalStorageDirectory() +File.separator+"p.apk");
String plugPath = file.getAbsolutePath();
File dir = this.getDir("plugDir", Context.MODE_PRIVATE);
DexClassLoader dexClassLoader = new DexClassLoader(plugPath,dir.getAbsolutePath(),null,getClassLoader());
Class baseClass2 = Class.forName("dalvik.system.PathClassLoader");
Field pathListField2 = baseClass2.getDeclaredField("pathList");
pathListField2.setAccessible(true);
Object mDexPathList2 = pathListField2.get(dexClassLoader);
Field dexElementsField2 = mDexPathList2.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElements2 = dexElementsField2.get(mDexPathList);
//第三步,创建新的dexElements:将宿主和插件的dexElements合并
int mainLength = Array.getLength(dexElements);
int plugLength = Array.getLength(dexElements2);
int sumLength = mainLength + plugLength;
Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumLength);
for (int i=0;i<sumLength;i++){
if (i<mainLength){
Array.set(newDexElements,i,Array.get(dexElements,i));
}else {
Array.set(newDexElements,i,Array.get(dexElements2,i - mainLength));
}
}
//第四步,将宿主的dexElements替换成新的dexElements
dexElementsField.set(mDexPathList,newDexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
如上,将宿主加载的DexElements替换成宿主和插件合起来的DexElements即可。