dex替换方式实现热修复

一、dex数组替换的原理

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的类给替换掉了。

二、代码实现验证

1、下载修复包到手机上

实际开发中修复包是放到服务器上的,当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);
2、自定义DexClassLoader加载修复包
//创建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的四个参数:

  • 第一个参数dexPath:要加载的包含dex的文件路径
  • 第二个参数optimizedDirectory:apk/jar等包解压后提取的dex的输出目录
  • 第三个参数librarySearchPath:c/c++代码的路径
  • 第四个参数parent:父类加载器,一般取当前类的getClassLoader
3、获取到自己的dexElements数组
//获取自己的dexElements数组
Object patchDexElements = DexUtils.getDexElements(DexUtils.getPathList(patchDexClassLoader));
4、获取到系统的dexElements数组
//获取系统原有的dexElements数组
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object sysDexElements = DexUtils.getDexElements(DexUtils.getPathList(pathClassLoader));
5、将两个dexElements数组合并为一个新的数组(自己的要放在最前面)
//合并两个dexElements数组(这里需要将自己的数组放前面以保证优先加载)
Object newDexElements = DexUtils.combineArray(patchDexElements, sysDexElements);
6、将合并后的新数组重新设置给dexElements变量
//重新将系统的dexElements替换为合并后的值
DexUtils.setDexElements(DexUtils.getPathList(pathClassLoader), newDexElements);

Demo源码:https://github.com/huihuigithub/blog_demo_projects (hot_fix_demo项目)

你可能感兴趣的:(Android)