Android Tinker 热修复原理

热修复流派

基于Multidex的Dex注入

代表:TInker,手机QQ空间、Nuwa
原理:将补丁Dex对象的DexFile对象注入到系统ClassLoader相关联的DexPathList对象的dexElements数组的最前面

Native层方法替换

代表:AndFix,阿里百川HotFix
原理:在Native层对方法的整体数据结构(Method/ArtMethod)进行替换

ClassLoader Hack

代表:Instant Run
原理:基于双亲委派机制,用自定义的IncrementalClassLoader加载补丁Dex,同时将该类加载器设置为系统类加载器的父加载器

Tinker 热修复Dex插桩原理

ClassLoader的加载路径可以包含多个dex文件,每个dex文件关联一个Element,多个dex文件排列成一个有序的数组dexElements,当查找类的时候会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找到则返回,如果找不到从下一个dex文件继续查找。理论上如果在不同的dex中有相同的类存在,那么优先选择排在前面的dex文件的类。
Android Tinker 热修复原理_第1张图片

源码分析

我们解压一个apk,可以看到有这些.dex分包。
Android Tinker 热修复原理_第2张图片
对应在代码里,BaseDexClassLoader中的pathList就是这些分包。

类加载器 ClassLoader
子类:BaseDexClassLoader

Android Tinker 热修复原理_第3张图片

findClass是来寻找指定的类,我们可以看到,会通过pathList来寻找类。
Android Tinker 热修复原理_第4张图片
pathList的类型是DexPathList ,我们再来看DexPathList,可以看到有一个dexElements属性,是Dex的集合。
Android Tinker 热修复原理_第5张图片
再来看DexPathList的构造方法
Android Tinker 热修复原理_第6张图片
可以看到,通过makePathElements方法,对dexElements进行赋值。
来看makePathElements方法

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions) {
    List<Element> elements = new ArrayList<>();
    
    //遍历文件
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();

        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()) {
        	//如果文件名包含 .dex 后缀
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                	//尝试加载Dex文件
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }

        if ((zip != null) || (dex != null)) {
        	//将dex文件传入Element中,并将element添加到elements集合中
            elements.add(new Element(dir, false, zip, dex));
        }
    }

  	return elements.toArray(new Element[elements.size()]);
}

可以看到,这里会遍历目录下的所有文件,然后,如果后缀匹配.dex,则尝试加载dex,如果加载成功,就将dex添加到elements集合中。
最终,将elements转为数组,并返回。

App安装

然后我们知道,App安装成功的时候,会复制一份apk文件到应用的私有目录
/data/app/packageName~1/base.apk

所以可以有思路:将修复好的x.dex下载到本机,插桩到dex集合的最前面,优先加载当中修复好的java类。

实现步骤
  1. 自己创建一个类加载器
  2. 用自己创建的类加载器加载 x.dex 修复包 (得到对于的DexElements[])
  3. 将得到dexElements和系统的dexElements[]进行合并,并且将自有的数据放置在最前面
  4. 通过反射的技术,将合成后的新数组,赋值给系统的pathList
    Android Tinker 热修复原理_第7张图片
修改后的java代码,如何生成dex ?
  1. 通过javac指令编译成class文件
  2. 通过dx.bat转成dex文件 (修复包)

核心步骤总结

1.获取系统ClassLoader的pathList对象

Object pathList = Reflect.on(loader).field("pathList").get();

2.调用makePathElements构造补丁的dexElements

ArrayList<IOException> suppressedExceptions = new ArrayList<>();
Object[] patchDexElements = makePathElements(pathList,extraDexFiles,FileUtil.getDexOptDir(context),suppressedExceptions);

3.将补丁Dex注入系统ClassLoader的pathList对象的dexElements的最前面

expandElementsArray(pathList,patchDexElements);

小结

以上就是QQ空间超级补丁技术,Tinker和QQ空间超级补丁技术的原理基本相同,但对部分缺点进行了优化。

  • 为了实现修复这个过程,在应用中必须多出一个专门用作修复用的patch.dex,如果修复的类到了一定数量,就需要花不少的时间加载。
  • 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。

Tinker针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。

具体详见阿里最新热修复Sophix与QQ超级补丁和Tinker的实现与总结

类校验异常的规避

Android Tinker 热修复原理_第8张图片
dex插装可能会导致该异常,所以需要破坏这3个条件的其中一个。对于Tinker,就是破坏第三个条件,就是通过全量后的方式,以避免打上预校验标识的类,直接使类不在同一个dex中。

你可能感兴趣的:(Android深度)