源码解析: 类加载+热更

103 阅读7分钟

动态修复bug

  1. 下发补丁(内含修复好的class)到用户手机,即让app从服务器上下载(网络传输)
  2. app通过**"某种方式"**,使补丁中的class被app调用(本地更新)

一,双亲委派机制 :

当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程

工作过程

一句话概括:ClassLoader 加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的 findClass 方法加载,该逻辑避免了类的重复加载。所以我们所要实现的就是把要替换的类可见性提前,这样类加载器就会优先找到修复过的类。

ClassLoader中: 

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

1)特点:

如果一个类被classLoader继承线路上的任意一个加载过,那么在以后整个系统的生命周期中,这个类都不会再被加载,大大提高了类的加载效率。

2)作用:

1,避免类的重复加载,JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就属于两个不同的类(比如,Java中的Object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,如果不采用双亲委派模型,由各个类加载器自己去加载的话,系统中会存在多种不同的Object类)

2,保护程序安全,防止核心API被随意篡改,避免用户自己编写的类动态替换 Java的一些核心类,比如我们自定义类:java.lang.String

3,在JVM中表示两个class对象是否为同一个类存在两个必要条件:

类的完成类名必须一致,包括包名

加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

二,Android中的ClassLoader

Android中最主要的类加载器有如下4个:

-   BootClassLoader:加载Android Framework层中的class字节码文件(类似java的Bootstrap ClassLoader)
-   PathClassLoader:加载已经安装到系统中的Apk的class字节码文件(类似java的App ClassLoader)
-   DexClassLoader:加载制定目录的class字节码文件(类似java中的Custom ClassLoader)
-   BaseDexClassLoader:PathClassLoader和DexClassLoader的父类

1)使用场景

先来介绍一下这两种Classloader在使用场景上的区别

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。

那我们如何将新的 DEX 文件插入到上述的类加载器中呢?观察它们的父类BootClassLoader,可以发现有个成员变量 DexPathList,它的属性中有一个 dex 数组,也就是说我们只要将替换的类添加到 dexElements 前面即可,这样类加载器跟据双亲委托模型(Parent Delegation Model)的机制就会使用先找到的类。

三,热修复的实现原理

经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已。

热修复类加载方案 :

类加载方案为什么需要重启呢?

这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的

  • QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载

      pre-verified错误: 假如原先有个DEX文件中类B引用了类A,旧的类A与类B在同一个DEX文件,则B会被打上CLASS_ISPREVERIFIED,现在修复DEX文件包含了类A,当类B某个方法引用到类A时尝试去解析类A。就会报错

  • Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素
    因为补丁包只是差异包,需要和本地的 dex 、资源等进行合成后,得到全量的 dex 才能被完整地使用。这样也就避免了热修复中 dex 的 pre-verify 问题,也减少了补丁包的体积

       Tinker资源替换: 合并原始apk,得到fix.apk

1.反射拿到ActivityThread对象持有的LoadedApk容器

2.遍历容器中LoadedApk对象,反射替换mResDir属性为补丁物理路径.

3.创建新的AssetManager, 并根据补丁路径反射调用addAssetPath将补丁加载到新的AssetManager中.

4.反射获得ResourcesManager持有的Resources容器对象.

5.遍历出容器中的Resources对象, 替换对象的属性为新的AssetManager, 并且根据原属性重新更新Resources对象的配置.

  • 饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组。

热更demo流程图: github.com/luxiao0314/… 

1,从sdk卡获取.zip,.dex,.jar,.apk文件,添加到set集合里面

image.png

四,Instant Run方案 :

Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码:


IncrementalChange localIncrementalChange = $change;//1

if (localIncrementalChange != null) {//2

localIncrementalChange.access$dispatch(

"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,

paramBundle });

return;

}

其中注释1处是一个成员变量localIncrementalChange ,它的值为changechange,change实现了IncrementalChange这个抽象接口。当我们点击InstantRun时,如果方法没有变化则changenull,就调用return,不做任何处理。如果方法有变化,就生成替换类,这里我们假设MainActivityonCreate方法做了修改,就会生成替换类MainActivitychange为null,就调用return,不做任何处理。如果方法有变化,就生成替换类,这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivityoverride,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的change设置为MainActivitychange设置为MainActivityoverride,因此满足了注释2的条件,会执行MainActivityoverrideaccessoverride的accessdispatch方法,accessdispatch方法中会根据参数"onCreate.(Landroid/os/Bundle;)V"执行MainActivitydispatch方法中会根据参数"onCreate.(Landroid/os/Bundle;)V"执行MainActivityoverride的onCreate方法,从而实现了onCreate方法的修改。 借鉴Instant Run的原理的热修复框架有Robust和Aceso。

美团 Robust 热修复方案原理

下面,进入今天的主题,Robust热修复方案。首先,介绍一下 Robust 的实现原理。

以 State 类为例

public long getIndex() {
    return 100L;

}

插桩后的 State 类

public static ChangeQuickRedirect changeQuickRedirect;

public long getIndex() {

    if(changeQuickRedirect != null) {

        //PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数

        if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {

            return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();

        }

    }

    return 100L;

}

//通过代理的形式,会调用StatePatch.accessDispatch实现返回值的替换

我们生成一个 StatePatch 类, 创一个实例并反射赋值给 State 的 changeQuickRedirect 变量。

public class StatePatch implements ChangeQuickRedirect {

    @Override

    public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {

        String[] signature = methodSignature.split(":");

        // 混淆后的 getIndex 方法 对应 a

        if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a

            return 106;

        }

        return null;

    }

    @Override

    public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {

        String[] signature = methodSignature.split(":");

        if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a

            return true;

        }

        return false;

    }

}