热更新之Tinker类加载原理


Android的基础语言是Java,而Java在运行时虚拟机加载对应的类和资源是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLoader类作为Android的默认的类加载器。

Android中的虚拟机不是JVM,而是Dalvik/ART VM。不同于JVM中加载.class,Dalvik/ART加载的是.dex文件。

类加载机制

每个类编译后产生一个Class对象,存储在.class文件中,JVM使用类加载器Class Loader来加载类的字节码文件.class,类加载器实质上是一条类加载器链,一般的,我们只会用到一个原生的类加载器,它只加载Java API等可信类,通常只是在本地磁盘中加载,这些类一般就够我们使用了。

如果我们需要从远程网络或数据库中下载.class字节码文件,那就需要我们来挂载额外的类加载器。也即类加载器中有多个Class Loader,平时只用一个默认的PathClassLoader,额外情况还需要DexClassPath。

加载策略

父类优先策略是比较一般的情况(如JDK采用的就是这种方式),在这种策略下,类在加载某个Java类之前,会尝试代理给其父类加载器,只有当父类加载器找不到时,才尝试自己去加载。
自己优先的策略与父类优先相反,它会首先尝试自己加载,找不到的时候才要父类加载器去加载,这种在web容器(如tomcat)中比较常见。

类的加载和初始化

类加载器加载了一个类的.class文件,不意味着该Class对象被初始化。一个类的初始化包括3个步骤:

  1. 加载(Loading),由类加载器执行,查找字节码,并创建一个Class对象(只是创建);
  2. 链接(Linking),验证字节码,为静态域分配存储空间(只是分配,并不初始化该存储空间),解析该类创建所需要的对其它类的应用;
  3. 初始化(Initialization),首先执行静态初始化块static{},初始化静态变量,执行静态方法(如构造方法)。

注意:类与接口的初始化不同,如果一个类被初始化,则其父类或父接口也会被初始化,但如果一个接口初始化,则不会引起其父接口的初始化。

动态加载

不管使用什么样的类加载器,类都是在第一次被用到时,动态加载到JVM的。这个特性就是Java的动态加载特性:

  1. Java程序在运行时并不一定被完整加载,只有当发现该类还没有加载时,才去本地或远程查找类的.class文件并验证和加载;
  2. 当程序创建了第一个对类的静态成员的引用(如类的静态变量、静态方法、构造方法——构造方法也是静态的)时,才会加载该类。

类的链接

Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程,它是保障加载的类能在虚拟机中正常运行的必要步骤。链接有3个步骤:

  1. 验证(Verification),验证是保证二进制字节码在结构上的正确性,包括检测类型正确性,
    接入属性正确性(public、private),检查final class 没有被继承,检查静态变量的正确性等。

  2. 准备(Preparation),准备阶段主要是创建静态域,分配空间,给这些域设默认值,
    需要注意的是两点:一个是在准备阶段不会执行任何代码,仅仅是设置默认值,二个是这些默认值是这样分配的,
    原生类型全部设为0,如:float:0f,int 0, long 0L, boolean:0(布尔类型也是0),其它引用类型为null。

  3. 解析(Resolution),解析的过程就是对类中的接口、类、方法、变量的符号引用进行解析并定位,
    解析成直接引用(符号引用就是编码使用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址),
    并保证这些类被正确的找到。
    解析策略:
    early resolution:要求所有引用都必须存在,所以在解析时递归的把所有引用解析
    late resolution:Oracle的JDK所采取的策略,在类只是被引用还未被真正用到时,并不进行解析,只有真正用到时才会加载和解析这个类。

ClassLoader

类加载器,ClassLoader。
ClassLoader的层级结构:
在这里插入图片描述
Android中Dalvik虚拟机中的类加载流程
在这里插入图片描述
Android的类加载器分为两种PathClassLoaderDexClassLoader,都继承自BaseDexClassLoader,而BaseDexClassLoader继承自ClassLoader。
PathClassLoader:用来加载系统类和应用类,已经缓存的dex,如ART的中的所有.dex,是Android虚拟机中的默认的加载器
DexClassLoader:用来加载jar.apk、dex文件,也可从SD卡中进行加载。加载jar、apk最终的实质也是提取了里面的Dex文件进行加载。

双亲委派模型

DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。
特点
即当一个加载器被请求加载某个类时,它首先委托自己的父加载器去加载,一直向上查找,若顶级加载器(优先)或父类加载器能加载,则返回这个类所对应的Class对象,若不能加载,则最后再由请求发起者去加载该类。
如果已经加载过了,就会直接将之返回,而不会重复加载。

【注:】这就是为什么采用类加载方案的热修复需要冷启动生效的原因:补丁合成好之前类已加载,想要替换bug类,需要重新启动软件,重新加载修复好的类
表现
类加载的时候会去遍历dex文件,优先加载前面的dex。类加载热更新就是应用重启时加载的就是已经修复问题的dex文件。
优点
这种方式的优点就是能够保证类的加载按照一定的规则次序进行,越是基础的类,越是被上层的类加载器进行加载,从而保证程序的安全性。

类加载器关键源码

1. ClassLoader
从源码中看出,虚拟机中默认的SystemClassLoader是PathClassLoader。

    /**
     * Encapsulates the set of parallel capable loader types.
     * 封装一组并行的加载器类型。
     */
    private static ClassLoader createSystemClassLoader() {
    
    
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        // 看见了吧 PathClassLoader 是默认的类加载器
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

2. BaseDexClassLoader

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    
    
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

BaseDexClassLoader除了调用了父类ClassLoader的构造方法,还在构造函数中初始化了一个DexPathList对象,这是一个描述DEX文相关资源文件的条目列表。

3. DexPathList

   final class DexPathList {
    
    
  ...
  
    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
    
    

        .......
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
    
    
            this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
    
    
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

    public Class findClass(String name, List<Throwable> suppressed) {
    
    
            //遍历该数组
            for (Element element : dexElements) {
    
    
                //初始化DexFile
                DexFile dex = element.dexFile;

                if (dex != null) {
    
    
                    //调用DexFile类的loadClassBinaryName方法返回Class实例
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
    
    
                        return clazz;
                    }
                }
            }
            return null;
        }
    
...}

从代码中可以看出,热修复至关重要的dexElements,就是在BaseDexClassLoader的初始化中进行初始化的。在DexPathList的构造函数中调用makeDexElements解析出dex相关参数,并保存到dexElements成员变量中,dexElements成员的顺序决定了.dex的加载顺序。

DexPathList的findClass方法就是为了检测扫描到的Class,该方法会遍历dexElements,然后获取每个Element的DexFile,DexFile不为空则调用其loadClassBinaryName并返回Class实例。ClassLoader在加载到正确的Class后,对同一Class将不再加载。
findClass方法是ClassLoader的核心

5. makeDexElements
在makeDexElements方法中loadDexFile方法加载dex文件,并返回DexFile对象。

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
    
    
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
    
    
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {
    
    
                // Raw dex file (not inside a zip/jar).
                try {
    
    
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
    
    
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
    
    
                zip = file;

                try {
    
    
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
    
    
                    /*
                     * IOException might get thrown "legitimately" by the DexFile constructor if the
                     * zip file turns out to be resource-only (that is, no classes.dex file in it).
                     * Let dex == null and hang on to the exception to add to the tea-leaves for
                     * when findClass returns null.
                     */
                    suppressedExceptions.add(suppressed);
                }
            } else if (file.isDirectory()) {
    
    
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
    
    
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
    
    
                elements.add(new Element(file, false, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }

6. dexElements

    List of dex/resource (class path) elements.Should be called pathElements,
    but the Facebook app uses reflection to modify 'dexElements'
    dex / resource(类路径)元素的列表。应称为pathElements,但Facebook应用程序使用反射来修改“ dexElements”

dexElements 是一个Element[] ,它是类加载模式热更新的核心。
它的作用是维护全部的dex文件(我们写的类的二进制表述方式,用来给安卓虚拟机加载),存在Android程序中。

安卓虚拟机会根据需要从该数组按照自上而下的顺序加载对应的类文件,即使数组中存多个同一个类对应的dex文件,虚拟机一旦找到了对应的dex文件就会停止查找,并加载。 根据这个规则,我们只需要把Bug修复涉及到的类文件插入到数组的最前面去,就可以达到修复的目的。

例如:当一个patch.dex放到了dexElements的第一位,那么当加载一个bug类A时,发现在patch.dex中发现修复过的类A,则直接加载这个类。在加载class.dex时也会扫描到未修复的类A,但是类A已被加载过,将不再重新加载A,即达到了修复的效果。

7. DexClassLoader

   public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath,ClassLoader parent) {
    
    
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

dexPath:加载APK、DEX和JAR的路径。多个路径之间可以用:分割
这个类可以用于Android动态加载DEX/JAR。
optimizedDirectory: dex文件首次加载时会进行dex opt操作,optimizedDirectory是优化后odex的存放目录,目录不为空,且官方推荐使用应用的私有目录,dexOutputDir = context.getDir(“dex”, 0)。
libraryPath:加载DEX的时候需要用到的lib动态库路径,libraryPath一般包括/vendor/lib和/system/lib。不可为空
parent:DEXClassLoader指定的父类加载器,ClassLoader参数类型

【注意】

  1. 这个类加载器加载的文件是.jar或者.apk文件,并且这个.jar或.apk中是包含classes.dex这个入口文件的,主要是用来执行那些没有被安装的一些可执行文件的。比如热更新中的Bugly的补丁包是.apk文件,Sophix生成的补丁是.jar文件。

  2. 这个类加载器需要一个属于应用的私有的目录作为它自己的缓存优化目录。这个目录也就构造函数的第二个参数(dex输出路径)

  3. 不要把上面第二点中提到的这个缓存目录设为外部存储,因为外部存储容易受到代码注入的攻击。

8. PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    
    
 
    public PathClassLoader(String dexPath, ClassLoader parent) {
    
    
        super(dexPath, null, null, parent);
    }
 
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
    
    
        super(dexPath, null, libraryPath, parent);
	}
} 

android系统采用PathClassLoader作为其系统加载器以及应用加载器。
PathClassLoader 和DexClassLoader的区别就在于optimizedDirectory参数是否为空

9. findLoadedClass
这个方法在ClassLoader中,其子类没有该方法。调用了findLoadedClass查找当前虚拟机是否已经加载过该类,是则直接返回该class。如果未加载过,则调用父加载器的loadClass方法,这里采用了java的双亲委派模型。

 protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    
    
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
    
    
            ClassNotFoundException suppressed = null;
            try {
    
    
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
    
    
                suppressed = e;
            }

            if (clazz == null) {
    
    
                try {
    
    
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
    
    
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

10.DexClassLoader与PathClassLoader的区别
在构造方法中PathClassLoader的optimizedDirectory参数可以为空,DexClassLoader中的参数不可为空。PathClassLoader会自动生成一个缓存目录/data/dalvik-cache/[email protected]。DexClassLoader是使用系统默认的缓存路径。

所以一般PathDexClassLoader只能加载已安装的apk的dex,而DexClassLoader则可以加载指定路径的apk、dex和jar,也可以从sd卡中进行加载。

在dex分包的时候,我们通过PathClassLoader获取已加载的保存在pathList中的dex信息,然后利用DexClassLoadder加载我们指定的从dex文件,将dex信息合并到pathList的dexElements中,从而在app运行的时候能够将所有的dex中的类加载到内存中。

类加载热更新原理

  1. 通过获取到当前应用的Classloader,即为BaseDexClassloader
  2. 通过反射获取到他的DexPathList属性对象pathList
  3. 通过反射调用pathList的dexElements方法把patch.dex转化为Element[]
  4. 两个Element[]进行合并,把合并的fix.dex放到dexElements最前面去
  5. 加载Element[],达到修复目的

Tinker中的热更新流程

在这里插入图片描述
在这里插入图片描述

  1. Tinker 方案参考了multidex的实现原理,在编译时通过新旧两个APK的Dex生成差异patch.dex。
  2. 通过相关平台将patch.apk下发到终端,patch.dex与旧版.dex合并还原成新的.dex。
  3. 将新合成的dex插入dexElements数组最前面,使得新的.dex中的内容Class优先加载
  4. 下次启动程序,bug得到修复

其中有些常识需要额外注意

  1. 为了减小patch的大小,Tinker自研了DexDiff算法,深度利用Dex的格式减小差异大小。让patch只包含有差异的部分,相同的部分不包含
  2. Tinker的补丁包形式是.apk格式,除了patch.dex,还包含resource差异包等文件
  3. 由于合并的过程比较费时,所以有一个单独的PatchService进行合并。
  4. 类加载实现原理涉及了dex文件的重新解压缩合并等处理,消耗的内存大,时间长,系统内存低时容易合并失败。也即类加载热更新存在一定的失败率。

QZone与Tinker的类加载有点区别, QZone不用patch.dex与旧的.dex合并,所以会出现CLASS_ISPREVERIFIED问题。而Tinker的patch.dex与旧的.dex合并后在同一个.dex中,其实是将patch.dex的内容顺序排在了修复.dex的前面,能优先加载.patch里面的类。

为弄清楚类加载机制的热更新专门写的该篇博客,希望能帮助大家理解基于类加载方案的热更新原理。

猜你喜欢

转载自blog.csdn.net/luo_boke/article/details/106219691