Android中比较常用的热修复框架是Sophix和Tinker,Sophix框架是通过修改方法指针来实现的,而Tinker框架是通过修改dex数组元素来实现的,这里就研究下dex替换方式的原理并通过代码实现来验证下。
Android中的类加载器主要是PathClassLoader和DexClassLoader,PathClassLoader是用来加载已经安装过的apk的dex文件,它不能自定义解压出的dex输出目录,这个主要是系统使用。DexClassLoader可以用来加载没有安装的apk/jar文件,它可以自定义解压出的dex输出目录,这个主要是自定义时使用。这两个类加载器都继承了BaseDexClassLoader,它们自身只有自己的构造方法,所以要看它们的类加载逻辑只需要查看BaseDexClassLoader就可以了,这个类加载器是通过findClass方法实现类的加载的,BaseDexClassLoader的findClass方法如下:
//注释1
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//注释2
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;
}
在注释2处调用了pathList的findClass方法来加载类(方法中的name参数是要加载的类的全类名),从注释1处可知pathList是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;
}
上述代码中可以看出,对dexElements数组进行循环遍历,然后调用每一个element元素的findClass方法去加载名称为name的类,如果加载成功就直接返回对应的Class以完成加载。在
DexPathList的构造方法中会通过调用makeDexElements方法生成dexElements数组,具体的生成过程是在makeDexElements方法中拿到classLoader传递过来的dexPath路径,然后将这个路径下的所有dex文件封装成一个一个独立的DexFile文件,然后再根据DexFile文件封装成一个一个相对应的Element对象,然后根据Element元素组成dexElements数组,这个过程就不粘贴源码了,有兴趣可以自行查看源码。
通过上述对类加载过程的分析可以知道,类的加载是通过遍历dexElements数组实现的,这样就可以自己生成修复bug后的dex文件,然后通过DexClassLoader将dex文件加载进来,把这些dex文件生成对应的Element添加到dexElements数组的最前边,当类进行加载的时候会先遍历自己的dex,如果加载成功就直接返回对应的Class对象完成加载,后面有bug的相同类就不会加载了,这样就悄悄的把有bug的类给替换掉了。
实际开发中修复包是放到服务器上的,当App启动的时候就去下载到手机上(最好存放到应用私有目录下,这样更加安全,不容易被误删除,便于加载使用)。这里为了方便就省去了下载的过程,直接将修复包放到assets目录下了。然后将assets目录下的修复包复制到应用的私有目录下,代码如下(以下代码在com.znh.hot.fix.HotFixUtils下的fixBug方法中):
//将修复的dex文件包复制到App的私有目录下(targetFilePath)
//targetFilePath:/data/user/0/com.znh.hot.fix/app_hui_patch/patch.apk
String assetFileName = "patch.apk";
String targetFilePath = context.getDir("hui_patch", Context.MODE_PRIVATE).getAbsolutePath() + File.separator + assetFileName;
FileUtils.copyFileFromAssets(context, assetFileName, targetFilePath);
//创建dex解压输出目录
String optimizedDirectory = context.getDir("hui_patch", Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "opt_dex";
File dirFile = new File(optimizedDirectory);
if (!dirFile.exists()) {
dirFile.mkdirs();
}
//创建DexClassLoader
DexClassLoader patchDexClassLoader = new DexClassLoader(targetFilePath, optimizedDirectory, null, context.getClassLoader());
DexClassLoader的四个参数:
//获取自己的dexElements数组
Object patchDexElements = DexUtils.getDexElements(DexUtils.getPathList(patchDexClassLoader));
//获取系统原有的dexElements数组
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object sysDexElements = DexUtils.getDexElements(DexUtils.getPathList(pathClassLoader));
//合并两个dexElements数组(这里需要将自己的数组放前面以保证优先加载)
Object newDexElements = DexUtils.combineArray(patchDexElements, sysDexElements);
//重新将系统的dexElements替换为合并后的值
DexUtils.setDexElements(DexUtils.getPathList(pathClassLoader), newDexElements);
Demo源码:https://github.com/huihuigithub/blog_demo_projects (hot_fix_demo项目)