热修复初探

3,486 阅读5分钟

热修复这个技术点最近有点火,有QQ空间开发团队为其背书,还有的大厂开源的热修复框架,这些对于推动这项技术也起了很大的作用。作为一个有追求的工程师菜鸟,今天,我想通过几篇文章把这种在线修复的解决思路以及几种具体的实现方案理一遍。

在开始分析之前,首先需要说说热修复解决了一个什么问题,闭上眼睛,想象一个场景:当你的产品刚上线,发现有一个导致闪退的空指针异常的问题或者程序重大漏洞急需解决,那么问题来了,怎么修复?常规的,当然是修改完有问题的方法,然后赶紧再打包,推送到用户强制更新,但是,现在还有另一种方式,就是程序在线下载修复文件到本地,然后用修改过的类覆盖原来有问题的类,这样的用户体验是一颗赛艇的。

好了,回到热修复这个话题。

首先我想先谈谈热修复本身,热修复是一种动态修复程序解决问题的思想,其本身是有很多不同的具体实现方案的,阿里的基于C/C++层操控method指针的Dexposed,AndFix,以及QQ空间的基于dex分包的HotFix,后者和前者的热修复方案在原理上截然不同,可以说各有千秋。而我在查阅资料的时候,发现很多Blog都不够严谨,往往标题声称热修复技术但是只解释QQ空间的解决方案,可以说这种做法是容易误导人的,虽然不能算错误,但是不太严谨。

目前的热修复技术的解决方案有很多,我想就上面提到的两种解决方案来做详细的探讨。

1,基于C层指针替换的Dexposed和AndFix

  • 这两个热修复的框架,在底层原理上是基本一致的,所以我想把他们放在一起探讨,

他们都做了大致三件事:

  • 1,在C/C++层将Java层中出问题的方法修改为native方法
  • 2,获取问题方法call到C层的指针
  • 3,通过获取的指针做相应的操作:调用Java层的回调方法继续处理(DexPosed)或者直接通过反射调用Java层的补丁方法(AndFix)。

以Dexposed为例:

dexposed.jpg

至于具体的代码解释,请直接看Android中免Root实现Hook的Dexposed框架实现原理解析以及如何实现应用的热修复

这两种热修复框架的区别在于:

  • Dexposed暂时不支持ART模式,AndFix支持
  • AndFix方案更加成熟,更加自动化(毕竟是支付宝出的)

2,基于Dex分包的HotFix

这个解决方案很巧妙,基于Google推出的的Multidex方案,以ClassLoader的方式完成对问题类的替换。

所以这个问题一定会先谈Android的分包方案:为了解决Android4.x系统中65536的方法数限制,Android推出Multidex方案,将一个完整的APK中的Dex拆分成好几个dex,通过PathClassLoader 这个加载器来加载。

当点开程序的时候,PathClassLoader 会把分包的多个dex添加到父类中的一个DexPathList 中

DexPathList 详情如下:

public class BaseDexClassLoader extends ClassLoader {

    private final DexPathList pathList;
}
/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    /** class definition context */
    private final ClassLoader definingContext;

    /** list of dex/resource (class path) elements  也就是dex列表咯*/
    private final Element[] dexElements;

    /** list of native library directory elements */
    private final File[] nativeLibraryDirectories;

那么当需要加载某个类的时候,是怎么加载的呢?

//BaseDexClassLoader:  
    @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;  
    }

findClass()方法如下:

 public Class findClass(String name, List<Throwable> suppressed) {      
         for (Element element : dexElements) {  
           DexFile dex = element.dexFile;  
            if (dex != null) {  
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);  
                if (clazz != null) {  
                    return clazz;  
                }  
            }  
       }  
        if (dexElementsSuppressedExceptions != null) {  
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));  
        }  
        return null;  
    }

如你所见,当需要加载一个类的时候,会在pathList中去寻找,并且是通过顺序遍历各个dex包的方式,一旦找到目标类,则停止遍历

qqZone.png

这就给了我们一个想法,有没有可能把打了补丁的dex插入到pathList中,当需要加载有问题的类的时候,根据遍历,首先查到已经修复的类,遍历结束,也就完成了修复。(当然了,这个想法是腾讯空间Android工程师想到的)

有了想法,也得有合适的加载器啊。结果你猜怎么着?Android还真提供了这样的机会。

在Android中也有三个类加载器,分别是UrlClassLoader,PathClassLoader,DexClassLoader.

  • UrlClassLoader 从Url列表中加载相关的jar文件,但是dalvik无法直接识别jar,so.....
  • PathClassLoader 它只会去读取 /data/dalvik-cache 目录下的 dex 文件,就是已安装的apk,
  • DexClassLoader 可以用来从.jar和.apk类型的文件内部加载classes、dex文件。而且,它和PathClassLoader继承自共同的父类。显然,这是最合适的加载器。

android.png

好了,基本机制到这里就结束了,还有一些问题却没有被提出来,不过网络上已经有了很好的解决方案了。

  • 如何防止自己的类被打上 CLASS_ISPREVERIFIED标志
    • 这个标志是虚拟机的一种优化手段,打上这个标志之后,就不会引用其他dex中的类,如果引用了,则报错。解决方案也很简单,就是在类中引用其他dex包的引用,具体方法请直接Google。

虽然现在我们公司的开发团队肯定用不上热修复技术,但是作为工程师却必须对新技术有所研究。近期我会继续研究热修复 HotFix 框架的源码,有必要的话会对DexClassLoader如何动态的插入jar包或者dex文件给出更详细的解析,暂时没有时间解释了,大家先上车

卧槽 说错话了.....