类加载机制原理解析

Android 高阶知识 博客系列目录

  1. 类加载机制原理解析
  2. ART与Dalvik、JVM之间的关系你懂了吗?


博客创建时间:2020.05.20
博客更新时间:2021.02.25


前言

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

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

Multidex
在android5.0之前,每个android应用只含有一个dex文件,dex的方法数量被限制在了65535之内,导致apk引入大量第三方sdk后方法数量超过限制无法编译通过。为了解决这个问题,Google推出多dex文件的解决方案multidex,一个apk可以包含多个dex文件。通过Multidex.install(this)完成dex文件的加载。

Android4.4时开始引入ART,在5.0以后终端默认虚拟机选择ART,ART是支持多个.dex文件的。

Dex文件
不管java和kotlin最终都要转成java来运行,在.java被编译成.class文件后,多个class文件会被打包成class.dex放在apk中。

dex文件是SDK使用dx命令生成的,我们可以用dx命令自己去打包想要打包的class。


类加载

在Android中,通过Dalvik/ART虚拟机去加载新的类,如果程序不重启,那么虚拟机中已加载的类就无法替换为新的类。

每个类编译后产生一个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{},初始化静态变量,执行静态方法(如构造方法)。

主动使用有以下6种:

  1. 创建类的实例
  2. 访问某个类或者接口的静态变量,或者对该静态变量赋值(如果访问静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)
  3. 调用类的静态方法
  4. 反射(Class.forName(xxx.xxx.xxx))
  5. 初始化一个类的子类(相当于对父类的主动使用),不过直接通过子类引用父类元素,不会引起子类的初始化
  6. Java虚拟机被标明为启动类的类(包含main方法的)

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


动态加载
不管使用什么样的类加载器,类都是在第一次被用到时,动态加载到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

类加载器,该类中的findClass方法是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. BaseDexClassLoader
BaseDexClassLoader除了调用了父类ClassLoader的构造方法,还在构造函数中初始化了一个DexPathList对象,这是一个描述DEX文相关资源文件的条目列表。

public class BaseDexClassLoader extends ClassLoader {
    
    
    private final DexPathList pathList;

  /**
     *
     * @param dexPath   加载APK、DEX和JAR的路径。这个类可以用于Android动态加载DEX/JAR。
     * @param optimizedDirectory  是DEX的输出路径
     * @param libraryPath   加载DEX的时候需要用到的lib库,libraryPath一般包括/vendor/lib和/system/lib。
     * @param parent  DEXClassLoader指定的父类加载器
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    
    
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

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

    /**
     * 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());
    }
}

public class DexClassLoader extends BaseDexClassLoader {
    
    

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

关于DexClassLoader,除了它的构造函数以外,它的源码注释里还提到以下三点:

  1. 这个类加载器加载的文件是.jar或者.apk文件,并且这个.jar或.apk中是包含classes.dex这个入口文件的,主要是用来执行那些没有被安装的一些可执行文件的。
  2. 这个类加载器需要一个属于应用的私有的,可以的目录作为它自己的缓存优化目录,其实这个目录也就作为下面,这个构造函数的第二个参数,至于怎么实现,注释中也已经给出了答案。
  3. 不要把上面第二点中提到的这个缓存目录设为外部存储,因为外部存储容易收到代码注入的攻击。

2. PathClassLoader

从源码中看出,虚拟机中默认的SystemClassLoader是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);
    }
}

3. DexPathList

在BaseDexClassLoader 构造函数中创建一个DexPathList类的实例,这个DexPathList的构造函数会创建一个dexElements 数组,它就是Tinker热修复的核心。

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

	//然后BaseDexClassLoader 重写了findClass方法,调用了pathList.findClass,跳到DexPathList类中.
    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将不再加载。此时将正确的Class放到DexFile中,并让这个DexFile放在dexElements数组前面即可。

findClass方法是ClassLoader的核心


4. 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()]);
    }

5. 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,即达到了修复的效果。


6. 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. 不要把上面第二点中提到的这个缓存目录设为外部存储,因为外部存储容易受到代码注入的攻击。


7. 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参数是否为空


8. 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;
    }

9.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中的类加载到内存中。


类加载热更新原理

Android框架中存在一个数组,它的作用是维护全部的dex文件(我们写的类的二进制表述方式,用来给安卓虚拟机加载),安卓虚拟机会根据需要从该数组按照自上而下的顺序加载对应的类文件,即使数组中存多个同一个类对应的dex文件,虚拟机一旦找到了对应的dex文件就会停止查找,并加载。根据这个规则,我们只需要把Bug修复涉及到的类文件插入到数组的最前面去,就可以达到热修复的效果。

Tinker的热修复实质就是利用Element[] dexElements的顺序来做文章。 当一个补丁的patch.dex放到了dexElements的第一位,那么当加载一个bug类时,发现在patch.dex中,则直接加载这个类,原来的bug类可能就被覆盖了。 一旦一个类被加载了,将不再重复加载同一个类。

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

以Tinker热修复的过程来反映类加载的步骤如下:
在这里插入图片描述


总结

类加载实现原理涉及了dex文件的重新解压缩合并等处理,消耗的内存大,时间长,系统内存低是容易合并失败。


相关链接

  1. 类加载机制原理解析
  2. ART与Dalvik、JVM之间的关系你懂了吗?

扩展链接:

  1. System.exit(0) 与 android.os.Process.killProcess(android.os.Process.myPid())

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

猜你喜欢

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