Android 热修复的简析(一)

998 阅读6分钟

前言

之前一直有朋友私下问我关于热修复的一些问题,年后终于有时间进行相关的的一些总结,我个人不才,也做了一些关于总结。那么,当下的热修复方案常用的能按技术分为三种:

  • 1.底层替换方法
  • instant run 方法
  • 基于类加载机制

至于前两种就简单说下,这次主要分析一下第三种。

1.底层替换方法:典型框架(阿里 AndFix)

andfix通过在native层hook java层的代码来实现方法替换,可以做到及时生效。感觉很厉害,而且这种方法颗粒度比替换class要小。但是阿里在几年前已经停止维护了,可能是由于ndk的方案系统兼容性不好吧。

2.Instant Run方法 : 典型框架:美团Robus)

这个框架的方式是在编译期间在每个方法内都插入类似下面一段代码:

注意是每个方法都要插入,最终生成的class肯定会代码增加不少吧。如果需要修改原来方法的逻辑,好像只需要修改changeQuickRedict的值就可以了(这个是我猜测的,等下看源码验证)。它也可以做到即时生效。

3.类加载机制:典型(Tinker热修复和QQZone热修复)

Android的类加载:

上面是盗取了lance老师的图,很清晰了展示了android classLoader的继承结构。首先要注意的几个重要的类:

1.BootClassLoader: ClassLoader的内部类兼子类,用来加载Android Framework的class文件。 比如Activity,Service,BroadcastReceiver等。

2.PathClassLoader: 用来加载自己写的程序中的类。 比如自定义的MainActivity,MyService等。

3.DexClassLoader: 功能上与PathClassLoader基本相同,只是在构造方法参数上不同,PathClassLoadaer使用默认opt目录,而DexClassLoader在8.0之前可以指定opt目录,但是8.0之后这个参数失效了。

4.BaseDexClassLoader:提供了findClass()的实现。它的两个子类都是使用它的findClass()

5.classLoader:提供了loadClass()实现,所有子类均使用它的loadClass()(BootClassLoader除外)。

了解了以上几点之后,下面追一下Dex文件加载的过程,这里要到源码了(Android8.0):

1.第一步:

首先我门加载class的时候都是从loadClass()开始的,上面提到应用程序的类加载器是PathClassLoader, 而它的loadClass使用的是ClassLoader的loadClass() 方法,那就先看一下ClassLoader#loadCalss():


protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);   // 1
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);   // 2
                    } else {
                        c = findBootstrapClassOrNull(name);  // 3
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {    // 4
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

代码1处:首先从已经加载的class中寻找。看一眼 findLoadedCalss()

/**
     * Returns the class with the given <a href="#name">binary name</a> if this
     * loader has been recorded by the Java virtual machine as an initiating
     * loader of a class with that <a href="#name">binary name</a>.  Otherwise
     * <tt>null</tt> is returned.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @return  The <tt>Class</tt> object, or <tt>null</tt> if the class has
     *          not been loaded
     *
     * @since  1.1
     */
    protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }

实际上使用了VMClassLoader#findLoadedClass方法,应为这是个native方法,就不追了。可以看出BootClassLoader和非BootClassLoader查找已经加载的class的逻辑是不一样的。

代码2处:父类不为空,调用父类的loadClass(),在父类中重复这一整个的过程。这里的父类指的不是继承关系的父类,而是一个成员属性:

   // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent

注意的是我们自己的类的使用的PathClassLoader里面的parent属性是BootClassLoader.

代码3处:父类为空,去系统类中寻找class, ClassLoader中的实现是返回null,子类中只有BootClassLoader进行了实现。所以只有在加载系统源码的时候进入这里才会有class返回,否则为空。

代码4处:父类和程序本身都没有加载过class,则由自身去加载。

类加载的这种机制java中和android中是一样的,叫做双亲委托机制。首先优先去父类类加载中去进行加载,父类加载到的话就是用父类加载的class,如果父类没有加载到再由自己去加载。

2. 第二步

看一下自身加载时,findClass() 做了什么。上文说过,findClass的实现只有在BootClassLoader和 BaseDexClassLoader中进行实现,这里关心的是BaseDexClassLoader#findClass:

@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;
    }

其中pathList的findClass方法:

/**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
    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的数组,在每一个节点中寻找要加载的class,如果找到,停止遍历,立即返回。

首先来看dexElements是什么:

/**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

这个数组对应的就是我们apk中的多个dex文件。

总结一下这个过程就是,顺序遍历我们apk中的每一个dex文件,查找我们的class有没有在里面,找到立即返回。

总结一下刚才的源码,各个类的配合流程如下:

基于类加载机制热修复的思路:

1.反射获取当前程序的PathClassLoader

2.反射获取DexPathClassLoader的pathList属性

3.反射获取pathList中的属性dexElements

4.把自己的补丁包 patch.dex 转化为 Elements[]数组 pathElements

5.将pathElements 插入到 dexElements最前面,得到新的 newElements

6.将newElements反射替换原来的dexElements

看起来思路还是挺简单的,下面看一下基于这种思路的 Tinker和 QZone两个框架的特点:

Tinker:

Tinker的特点是有一个DexDiff机制, 需要提供一个Base.apk的基准包,通过这个基准包和当前apk的差异自动生成差分包patch.dex,运行时重新加载差分包。

QZone:

QQ空间基于Dex的分包方案,修复bug之后,放到一个单独的dex文件中。在我理解的QZone和Tinker的区别就是在于生成补丁包的方式: QZone 不会自动生成差分包dex,而是将有bug的class 单独打包成一个 dex, 后面加载我的门生成的dex的过程是类似的,都是进行插队。

先在在这里抛砖引玉,希望同道中人可以关注留言,相互讨论,谢谢。