Android 类加载机制

1.类加载机制

我们编写的.java文件不是可执行的文件,需要先编译成.class文件才可以被虚拟机执行。而类加载就是指通过类加载器把class文件加载到虚拟机的内存空间,具体来说是方法区。类通常是按需加载,即第一次使用该类时才加载。

Java与Android都是把类加载到虚拟机内存中,然后由虚拟机转换成设备可识别的机器码。由于它们使用的虚拟机不同,所以在类加载方面也有所区别。Java的虚拟机是JVM,Android的虚拟机是dalvik/art(5.0以后虚拟机是art,是对dalvik的一种升级)。Java虚拟机运行的是class文件,而Android 虚拟机运行的是dex文件。dex其实是class文件的集合,是对class文件优化的产物,为了避免出现重复的class。

先了解一下Android的运行流程:

①Android程序编译的时候,会将.java文件编译成.class文件;

②然后将.class文件打包为.dex文件;

③然后Android程序运行的时候,Android的Dalvik/art虚拟机就加载dex文件;

④加载其中的.class文件到内存中来使用。

当需要使用某个类时,虚拟机就会加载它的Class文件,并创建对应的Class对象,将Class文件加载到虚拟机的内存里,这个过程称为类加载。

类加载流程:

一个类被加载到虚拟机内存中需要经历加载、连接、初始化几个过程。其中连接分为三个步骤:验证、准备、解析。

27ef03576af747ca8b96b84c072bddcb.png

①步骤一:加载:将外部的Class文件加载到JVM虚拟机内,并存储到方法区。

加载过程主要做了三件事:

1)通过类的全限定名来获取定义此类的二进制字节流。主要是获取一个类的二进制字节流,意思就是把类以流的形式加载进内存,类的来源没有说,可以是jar包,也可以是class文件或者是apk文件。这个特性是能够实现插件化技术的理论基础。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。就是在获取到这个字节流以后,虚拟机就会把类中的静态存储结果保存到方法区中,保存的过程会转化对应方法区中的数据结构,所以说静态的结构都保存在内存中的方法区中。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的方位入口。当类加载进内存以后,每个类都会生成一个对应的Class对象,当使用这个类的时候,都是通过此Class对象为入口来使用的,比如写程序的时候通过new关键字创建一个类的对象的时候,也是通过这个类的Class对象来创建的。

②步骤二:验证:确保加载到Class文件里的信息符合虚拟机要求。主要是对类中的语法结构是否合法进行验证,确认类型符合Java语言的语义。

③步骤三:准备:为类变量分配内存,并设置类变量的初始化值(初始值通常为0,非开发者定义的值)。这个阶段是给类中的类变量分配内存,设置默认初始值,比如一个静态的int变量初始值是0,布尔变量初始值是false。

④步骤四:解析:将常量池内的符号引用转为直接引用,如hello()方法,hello是符号引用,地址值是直接引用。也就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。这个过程可以理解为一开始虚拟机对加载到内存中的各种类、字段等并没有一一编号,只是通过一个符号去表示,在解析阶段,虚拟机把内存中的类、方法等进行统一管理起来。

⑤步骤五:初始化:对类变量进行初始化。初始化阶段才真正到了类中定义的java代码的阶段,在这个阶段会对类中的变量和一些代码块进行初始化,比如以类变量进行初始化,在准备阶段对类变量进行的默认初始化,到这个阶段就对对变量进行显式的赋值,其中静态代码块就是在这个阶段来执行的。

注意:初始化不会马上执行,当一个类被主动使用的时候才会去初始化,主要有下面这几种情况:

1)当创建某个类的新实例时(如通过new或者反射等);

2)当调用某个类的静态方法时;

3)当使用某个类或接口的静态字段时;

4)当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时;

5)当初始化某个子类时;

类从被加载到JVM虚拟机内存到被卸载,整个完整的生命周期包括:类加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载 七个阶段,其中验证、准备、解析这三个部分统称为连接。

类加载流程比较复杂,虽然加载过程有复杂的五个步骤,但是开发者能够控制的只有第一步「加载」还有最后一步「初始化」,第一步记载的理论基础决定了插件化可以实现,最后一步初始化就是执行实际程序中的代码。其余都是由虚拟机控制的。

类加载的主要作用:

①实现类的加载功能

②确保被加载类在虚拟机中的唯一性

2.Android中的类加载器

系统通过ClassLoader将类加载到内存中,然后解析使用。在java中也有ClassLoader,但是因为java编译出来的是Class文件,而Android的APK中包含的是dex文件,dex文件是将所需的所有Class文件重新打包,打包的规则不是简单地压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,所以java中和Android中的ClassLoader也不一样,这里主要来看一下Android中的ClassLoader。

在类加载进内存以后,Android程序通过ClassLoader类去加载内存中的类,然后进行解析运行。

2aeaf4995a2c4ed8af7f39e77bc508b1.png

Android中包含以下几种类加载器:

①BootClassLoader :用来加载Framework层的字节码文件。只能加载Android系统的类,是ClassLoader的内部类,开发者无法调用。Android系统启动时会使用BootClassLoader来预加载常用类。

②BaseDexClassLoader :PathClassaLoader、DexClassLoader父类;

③PathClassLoader :加载内存中已经安装的apk中的dex文件。通常用于加载APK中我们自己写的类(含三方库);

④DexClassLoader :加载指定目录中的字节码文件,可以加载内存以外的aar/apk/jar文件。通常用于执行动态加载,能够加载指定路径的apk/jar/zip/dex文件, 因此很多热修复和插件化方案都采用;(DexClassLoader可以指定odex的路径,而PathClassLoader则采用系统默认的缓存路径,在8.0以后没有区别。)

⑤URLClassLoader:加载.jar文件和文件夹中的class,javaWeb等使用,谷歌不用

BaseDexClassLoader为核心类。

常用的ClassLoade就两个:DexClassLoader 和 PathClassLoader,这两个类的源码都很简单:

/**A class loader that loads classes from {@code .jar} and {@code .apk} files containing a {@code classes.dex} entry. This can be used to execute code not installed as part of an application.*/

public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {

        super(dexPath, new File(optimizedDirectory), libraryPath, parent);

    }

}

DexClassLoader类中就只有一个构造方法,构造方法中直接调用了父类的构造,DexClassLoader继承了BaseDexClassLoader,构造方法中的参数的含义是:

参一 dexpath:要加载的dex文件的路径。

参二 optimizedDirectory:dex文件首次加载时会进行优化操作,这个参数即为优化后的odex文件的存放目录,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir(“dex”, 0);

参三 libraryPath:动态库的路径。

参四 parent:当前加载器的父类加载器。

接着看PathClassLoader:

/**Provides a simple {@link ClassLoader} implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).*/

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

    }

}

PathClassLoader有两个构造方法,同样也是直接调用了父类的构造方法,从构造方法上来看,DexClassLoader和PathClassLoader的区别只有第二个参数optimizedDirectory,在PathClassLoader中optimizedDirectory默认传入的是null。

从源码中看这两个类的作用也是因为optimizedDirectory参数的不同而不同,在源码中看使用PathClassLoader由于没有传入optimizedDirectory,系统会自动生成以后缓存目录,即/data/dalvik-cache/,在这个目录存放优化以后的dex文件。

所以PathClassLoader只能加载已安装的apk的dex,即加载系统的类和已经安装的应用程序(安装的apk的dex文件会存储在/data/dalvik-cache中),而DexClassLoader可以加载指定路径的apk、dex,也可以从sd卡中进行加载。

在DexPathList的makeDexElements方法中,对于dex文件,需要调用loadDexFile方法来生成一个DexFile。

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements) throws IOException {

    if (optimizedDirectory == null) {

        return new DexFile(file, loader, elements);

    } else {

        String optimizedPath = optimizedPathFor(file, optimizedDirectory);

        return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);

    }

}

这里会判断optimizedDirectory是否为空,在PathClassLoader中传入的参数为空,那么在DexClassLoader中传入了这个路径,会调用DexFile的loadDex方法。

因为apk其实也是一个压缩文件zip包,像第一次启动时,PathClassLoader会将apk解压存在/data/dalvik-cache目录下,而使用DexClassLoader则是会将apk中可运行的文件提取出来,存放在optimizedDirectory路径下,那么应用程序启动时将会加载optimizedDirectory下的文件,启动速度更快,这就是odex优化。

3.双亲委托机制

双亲委托机制即父委托机制,指多个类加载器之间存在父子关系,当一个ClassLoader去加载一个类的时候,它会去判断该类是否已经加载,如果没有,它不会马上去加载,而是委托给父加载器进行查找,这样递归一直找到最上层的ClassLoader类,因此所有的类加载都会委托给顶层的父类即BoostrapClassLoader进行加载,如果找到了,就直接返回这个类所对应的Class对象,如果都没有加载过,就从顶层的ClassLoader去开始依次向下查找,每个加载器会从自己规定的位置去查找这个类,如果没有,最后再由请求发起者去加载该类。

d8420d11d69c4906ba58b2a2a68e0b28.png

 简单说,就是第一次查找的时候,是从下到上依次从缓存中查找之前有没有加载过,如果有就返回,如果都没有,就从上到下从自己制定的位置去查找这个类,最后在交给发起者去加载该类。这时候如果子加载器不能加载,则抛出ClassNotFoundException异常。

双亲委托机制的代码流程在java.lang.ClassLoader#loadClass()中:

protected Class<?> loadClass(String name, boolean resolve) {

    Class<?> c = findLoadedClass(name);

    //检查类是否被加载过

    if (c == null) {

        try {

            //如果没有加载,调用父类的加载器

            if (parent != null) {

                c = parent.loadClass(name, false);

            } else {

                //父类加载器为空,使用默认的 启动类加载器

                c = findBootstrapClassOrNull(name);

            }

        } catch (ClassNotFoundException e) {

        }

        if (c == null) {

            //父类加载器无法加载,则调用自身的findClass()进行类加载

            c = findClass(name);

        }

    }

    return c;

}

注意,这里有一个误区:

虽然PathClassLoader继承自BaseDexClassLoader,但是PathClassLoader的父类加载器并不是BaseDexClassLoader,从前面的例子中也可以看到,它的父类是parent,这里不要认为父类就是父类加载器,这是两个概念。

双亲委托机制的好处:

①保证class只会被加载一次,能有效确保一个类的全局唯一性,也就是说类的数据结构只会在第一次创建的时候被加载进内存(方法区),以后要创建这个类的对象的时候,直接用方法区中的class在堆内存创建一个对象,这样的话创建对象就会比较快;

②保证系统类的安全性。因为在启动应用进程的时候就已经加载好了系统类(BootClassLoader),那后面运行期就不可能通过恶意伪造加载的方式去造成一些系统安全问题。因为虚拟机认为只有两个类名一致并且被同一个类加载器加载的类才是同一个类,所以这种机制保证了系统定义的类不会被替代。

如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类。还有,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。

4.ClassLoader加载类的过程

从源码看一下Android中ClassLoader的加载类的整个流程。

首先需要获取DexClassLoader或者PathClassLoader,然后系统会调用loadClass方法来动态加载某个类。比如下面这样创建对象,然后调用loadClass方法加载某个类:

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), null, getClassLoader());

在DexClassLoader、PathClassLoader和BaseDexClassLoader中都没有找到loadClass方法的实现,它的实现在他们的父类ClassLoader中,代码如下:

ClassLoader.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;

}

loadClass方法中首先去询问父加载器有没有加载过,如果没有加载过会使用参数传递进来的父加载器去加载,如果还是没有加载过,会使用当前创建的加载器去加载该类,整个流程双亲委托机制。

接下来看看findLoadedClass方法中的实现:

ClassLoader.java:

protected final Class<?> findLoadedClass(String className) {

    ClassLoader loader;

    if (this == BootClassLoader.getInstance())

        loader = null;

    else

        loader = this;

    return VMClassLoader.findLoadedClass( loader, className);

}

如果该类还没有加载过,这个方法会返回null,然后接着会调用parent.loadClass(className, false) 方法,parent是传入的加载器,通过Context的getClassLoader方法返回的,在Context中getClassLoader是个抽象方法,具体的实现在ContextImpl中,代码如下:

ContextImpl.java:

@Override

public ClassLoader getClassLoader() {

    return mPackageInfo != null ?mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();

}

ClassLoader.java:

public static ClassLoader getSystemClassLoader() {

    return SystemClassLoader.loader;

}

//SystemClassLoader为ClassLoader的内部类

static private class SystemClassLoader {

    public static ClassLoader loader = ClassLoader.createSystemClassLoader();

}

private static ClassLoader createSystemClassLoader() {

    String classPath = System.getProperty( "java.class.path", ".");

    //最终返回了PathClassLoader作为系统加载器SystemClassLoader,而其父类为根加载器BootClassLoader

    return new PathClassLoader(classPath, BootClassLoader.getInstance());

}

上面的源码中可以看到,最后返回的是一个PathClassLoader对象,这个对象的父加载器是BootClassLoader,BootClassLoader是ClassLoader的一个内部类,从分析来看是Android平台上所有ClassLoader的最终parent。

返回PathClassLoader以后,它会接着调用loadClass,很显然最终会来到BootClassLoader中的loadClass方法中,代码如下:

BootClassLoader.java:

@Override

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

     Class<?> clazz = findLoadedClass( className);

     if (clazz == null) {

          clazz = findClass(className);

     }

     return clazz;

}

可以看到它在调用完findLoadClass以后,由于它已经是根加载器,所以肯定返回null,然后直接就调用了findClass方法,到这里再回看上面的ClassLoader加载图,就已经到达了最顶端,开始向下查找,最后一级一级的到达BaseDexClassLoader中的findClass方法中,代码如下:

BaseDexClassLoader.java:

@Override

protected Class<?> findClass(String name) throws ClassNotFoundException {

    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();

    //从pathList中去查找类

    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;

}

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {

    super(parent);

    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

}

从现在开始进入到真正的加载类的过程了,上面的全部都是在说ClassLoader按照双亲委派机制流程一层层的查找。

上面最重要的就是第二行代码,从pathList对象中去查找对应的类,查找不到会抛出异常,找到了直接返回。pathLIst 是一个DexPathList对象,它是在BaseDexClassLoader的构造方法中完成了初始化:

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) {

    super(parent);

    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

    if (reporter != null) {

        reportClassLoaderChain();

    }

}

其中每个参数的意义:

dexPath:目标类所在的apk、dex或者jar文件的路径(SD卡也可以),这个路径可以是多个路径,使用分隔符:分开;

librarySearchPath:加载程序文件时需要用到的so库的路径;

parent:当前类加载器的父加载器。

DexPathList.java:

DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) {

    ······

    // save dexPath for BaseDexClassLoader

    this.dexElements = makeDexElements( splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted)

}     

在DexPathList的构造方法中,初始化了一个Element数组:

DexPathList.java:

private static Element[] makeDexElements( List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {

    Element[] elements = new Element[files.size()];

    int elementsPos = 0;

    //遍历所有dex文件(也可能是jar、apk或zip文件)

    for (File file : files) {

        if (file.isDirectory()) { //如果当前文件是一个文件夹

            elements[elementsPos++] = new Element(file);

        } else if (file.isFile()) { //如果是一个文件

            String name = file.getName();      

            DexFile dex = null;

            if (name.endsWith(DEX_SUFFIX)) { //如果是dex文件

                try {

                dex = loadDexFile(file, optimizedDirectory, loader, elements);

                if (dex != null) {

                    elements[elementsPos++] = new Element(dex, null);

                }

              } catch (IOException suppressed) {

                  System.logE("Unable to load dex file: " + file, suppressed);

                  suppressedExceptions.add( suppressed);

              }

          } else { //如果是apk、jar、zip文件

            try {

                  dex = loadDexFile(file, optimizedDirectory, loader, elements);

              } catch (IOException suppressed) {

                  suppressedExceptions.add( suppressed);

              }

            if (dex == null) {

                  elements[elementsPos++] = new Element(file);

              } else {

                  elements[elementsPos++] = new Element(dex, file);

              }

          }

        //把剩余的文件拷贝到elements数组中

    if (elementsPos != elements.length) {

      elements = Arrays.copyOf(elements, elementsPos);

    }

    return elements;

}

通过DexPathList的makeDexElements方法会将apk中的dex文件存放到dexElements数组当中,调用DexPathList的findClass方法,遍历dexElements数组,从数组中找到这个类然后加载。

其实Android的类加载器(不管是PathClassLoader还是DexClassLoader),它们最后在加载文件时,都只认dex文件,而loadDexFile方法是加载dex文件的核心方法,它可以从jar、apk、zip中提取出dex。

接下来看看pathList的findClass方法是怎么实现的:

DexPathList.java:

private final Element[] dexElements;

//... 省略一大堆代码

public Class findClass(String name, List<Throwable> suppressed) {

     //遍历dexElements数组,拿到里面的dex

    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;

}

在findClass方法中遍历数组dexElements获取到里面的dex返回,dexElements里面保存的是apk中所有的dex。采用DexFile的loadClassBinaryName方法来加载class,是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。

通过以上分析,可以发现整个类加载流程就是:

①类加载器BaseDexClassLoader先将dex文件解析放到pathList到dexElements里面

②加载类的时候从dexElements里面去遍历,看哪个dex里面有这个类就去加载,生成class对象

这个也是Tinker实现的原理,Tinker热修复就是将补丁dex插入到dexElements最前端,这样classLoader就会先加载补丁包中修复了bug的class文件,由于classLoader双亲委托,再加载原先有bug的class文件时,发现有一模一样的修复了bug的class被加载了,就会直接返回不会再去加载旧class文件,从而完成修复bug的目的。

到这里ClassLoader加载dex的流程基本上就完毕了,总结一下:

①加载一个类是通过双亲委托机制来实现的;

②如果是第一次加载class,那是通过BaseDexClassLoader中的findClass方法实现的;接着进入DexPathList中的findClass方法,内部通过遍历Element数组,从Element对象中去查找类;Element实际上是对Dex文件的包装,最终还是从dexfile去查找的class;

③一般app运行主要用到2个类加载器,一个是PathClassLoader:主要用于加载自己写的类;另一个是BootClassLoader:用于加载Framework中的类;

④热修复和插件化一般是利用DexClassLoader来实现;

⑤PathClassLoader和DexClassLoader其实都可以加载apk/jar/dex,区别是 DexClassLoader 可以指定 optimizedDirectory,而PathClassLoader 只能使用系统默认位置。但是在8.0 以后二者是没有区别的,只能使用系统默认的位置了。

BaseDexClassLoader小结:

①首先在BaseDexClassLoader构造方法内创建了PathDexList对象;

②然后在DexPathList构造方法中,通过makeDexElements()等方法经过一些列调用,把dex文件做优化再缓存到指定目录,如果是包含dex的apk/jar/zip等压缩文件的话,会先解压再优化缓存,最后得到DexFile对象;

③将DexFile对象包装成Element对象,然后加到Element[] 数组。

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/125806960