关于Android-类加载

之前的文章说过 JVM 中负责将我们编写的 .java 文件翻译成 .class 字节码文件。而类要先经过 JVM 的 ClassLoader(类加载器)加载到 JVM 中然后再存储到运行时数据区最后经过执行引擎执行 类 中相应的方法,最后转化成机器码交给系统执行。

本文要讲的就是 Android 中的 ClassLoader(类加载器)要讲 Android 中的 ClassLoader,就要先讲 Android 中的虚拟机。由于 Android 每年都会发布新的版本,因此就存在多种 虚拟机 ,接下来的 Android 虚拟机的介绍将根据版本的演进进行介绍。

Dalvik 虚拟机

这个虚拟机出现在 Android 4.4 的机器上,DVM也是实现了JVM规范的一个虚拟机,默认使用CMS垃圾回收器,但是与JVM运行 Class 字节码不同,DVM执行 Dex(Dalvik Executable Format) ——专为 Dalvik 设计的一种压缩格式。Dex 文件是很多 .class 文件处理压缩后的产物,最终可以在 Android 运行时环境执行。
JVM 一个类编译后一个 .class 文件就是一个类,而 DVM 编译后一个 .dex 文件含有多个类:

JVM与DVM

基于寄存器

之前的文章已经介绍过 JVM 中运行时数据区的构成(栈区,堆区,方法区)所以我们说 JVM 是一种基于栈的虚拟机,而 Android 中的虚拟机略有不同,Android 中的虚拟机是基于寄存器的。

什么是寄存器?
可以简易的理解为寄存器就是 CPU 中一个存储数据的单元(CPU中有许多寄存器,比如负责计算的寄存器,存储数据的寄存器)。

基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上

JVM 与 DVM
Dalvik虚拟机执行的是dex字节码,解释执行。从 Android 2.2版本开始,支持JIT即时编译(Just In Time)指的是,在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化。

ART 虚拟机

ART(Android Runtime) 是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认 Android 运行时。ART虚拟机执行的是本地机器码。Android 的运行时从 Dalvik 虚拟机替换成 ART 虚拟机,并要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含dex字节码的文件。

那 ART 虚拟机执行的本地机器码是从哪里来的呢?
Dalvik 虚拟机下应用在安装的过程,会执行一次优化,将 dex 字节码进行优化生成odex 文件。而在 ART 虚拟机下,ART 引入了预先编译机制(Ahead Of Time),在安装时 ART 使用设备自带的 dex2oat 工具来编译应用,dex 中的字节码将被编译成本地机器码。

因此使用了 ART 虚拟机的设备安装应用时会比较慢,原因就是在安装时多做了一步编译成机器码的操作。

Android N 的运行机制

1. 在一开始的安装的时候,不会再执行 AOT(预编译机制),运行的过程中进行解释执行,对于一些经常执行的方法进行 JIT,经过 JIT 编译的方法会记录到 Profile 配置文件中。
2. 当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行 AOT 编译。待下次运行时直接使用。

Android N 的运行机制

ClassLoader

说完 Android 中的虚拟机,就可以来讲其中的 ClassLoader(类加载器)。我们先来看看源码中 ClassLoader 的结构吧:

ClassLoader结构
其中,ClassLoader 是个抽象类,因此可以看到下面是它的几个实现类,除了这几个实现类之外,还有一个类我们应该关注的就是 BootClassLoader,同时要关注的还有 BaseDexClassLoader以及其子类 PathClassLoader。
ClassLoader

  • BootClassLoader:用于加载Android Framework层class文件。
  • PathClassLoader:用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader:用于加载指定的dex,以及jar、zip、apk中的classes.dex
    下面先来看看 PathClassLoader 与 DexClassLoader 的源码区别吧:
    DexClassLoader & PathClassLoader
    可以看到,两者唯一的区别是在创建 DexClassLoader 的时候传入了一个 optimizedDirectory 参数,并且会将其创建为 File 传递给 super(父类),而 PathClassLoader 则是直接传 null。。因此两者都可以加载指定的dex,以及jar、
    zip、apk中的classes.dex。
    下面来看看两者的创建的方式:
    创建 PathClassLoader 与 DexClassLoader
    其实, optimizedDirectory 参数就是dexopt的产出目录(odex)。那 PathClassLoader 创建时,这个目录为null,就意味着不进行dexopt?并不是, optimizedDirectory 为null时的默认路径为:/data/dalvik-cache。
    在API 26源码中,将DexClassLoader的optimizedDirectory标记为了 deprecated 弃用,实现也变为了:
    API 26 中 DexClassLoader 的创建形式
    在这里可以看到,DexClassLoader 其实已经和 PathClassLoader 一样了。

loadClass

前面介绍了几种不同的ClassLoader,在进行类加载的时候执行的是 loadClass 方法。这里来看看 PathClassLoader 中的 loadClass 方法。

PathClassLoader
从上面可以看到 PathClassLoader 中不存在 loadClass 方法,根据 Java 多态的特点,PathClassLoader 里找不到去它的父类去找,最后在 ClassLoader 找到了 loadClass 的实现方法:

//BootClassLoader传进来的参数
private final ClassLoader parent;
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
            // 检查这个类是否已经被加载过
            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) {
                    //如果仍然找不到,则由实现类的findClass 方法去加载
                    c = findClass(name);
                }
            }
            return c;
    }
================================================
private Class findBootstrapClassOrNull(String name)
    {
        return null;
    }
================================================
protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

从上面源码看到,传进 ClassLoader 中 parent 的参数是 BootClassLoader,findClass 的第一个传参是要加载的类的全类名,一开始的话会先去检查该类是否已经被加载过,如果没有被加载过,则会优先使用 BootClassLoader 进行加载,如果 BootClassLoader不存在则会通过 findBootstrapClassOrNull 这个方法进行加载,在 Android 中这个方法的实现是直接返回 null(在 Java 中不同)因此直接走了对应实现类的 findClass 方法。

双亲委托机制

指的就是上面这种先父类去寻找类,父类找不到了再由子类去寻找。使用这种机制有什么好处呢?

  • 避免重复加载:当父加载器已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。
  • 安全性考虑:**如果没有双亲委托机制,自己定义一个与系统类相同类名的类,在进行加载的时候就会先找到我们自定义的类,从而篡改了系统源码,因此使用双亲委托机制也是出于安全性的考虑。

findClass

前面说到如果父类找不到该类,则由子类去寻找,下面就来看看 PathClassLoader 中 findClass 的实现,发现 PathClassLoader 中没有 findClass 方法,则继续往父类寻找,在 BaseDexClassLoader 中找到了 findClass 的实现:

public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        //通过 DexPathList 来创建 pathList 
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        //通过这个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;
    }

从 BaseDexClassLoader 中看到类是通过 pathList 这个值的 findClass 方法去寻找,找到了就返回,找不到就抛出错误。pathList 是在构造方法里新建的一个 DexPathList。那接下来去看 DexPathList 里面的 findClass 做了什么:

public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
        //一系列的判空
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexFiles == null) {
            throw new NullPointerException("dexFiles == null");
        }
        if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
            throw new NullPointerException("dexFiles contains a null Buffer!");
        }

        this.definingContext = definingContext;
        // TODO It might be useful to let in-memory dex-paths have native libraries.
        this.nativeLibraryDirectories = Collections.emptyList();
        this.systemNativeLibraryDirectories =
                //分割字符
                splitPaths(System.getProperty("java.library.path"), true);
        this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);

        ArrayList suppressedExceptions = new ArrayList();
        this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }
============================================
private static List splitPaths(String searchPath, boolean directoriesOnly) {
        List result = new ArrayList<>();

        if (searchPath != null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                if (directoriesOnly) {
                    try {
                        StructStat sb = Libcore.os.stat(path);
                        if (!S_ISDIR(sb.st_mode)) {
                            continue;
                        }
                    } catch (ErrnoException ignored) {
                        continue;
                    }
                }
                result.add(new File(path));
            }
        }

        return result;
    }
===========================================
 private static makeDexElements[] makePathElements(List files) {
        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
        int elementsPos = 0;
        for (File file : files) {
            String path = file.getPath();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                File zip = new File(split[0]);
                String dir = split[1];
                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
            } else if (file.isDirectory()) {
                // We support directories for looking up native libraries.
                elements[elementsPos++] = new NativeLibraryElement(file);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }
==============================================
public Class findClass(String name, List 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;
    }
=================================================
public Class findClass(String name, ClassLoader definingContext,
                List suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

一开始实例的时候,splitPaths 这个方法分割传进来的字符,也就是 dex 文件的全路径,跟到 splitPaths 这个方法里可以看到,根据 File 文件的分隔符进行分割,并且返回一个全新 File 类型的 List,List 里不 add 进 File。接着通过 makePathElements 这个方法,根据 splitPaths 这个方法生成 File List 生成一个 Elements 的数组,一个 dex 文件就是一个 Element。

最后在 findClass 这个方法中,根据传进来的全类名路径在 makeDexElements 这个方法生成的 Elements 数组中遍历每个 Element 的 findClass 方法,由于每个 Element 中含有一个 dexFile,一个 dex 文件转化为一个 dexFile,然后调用 dexFile 的 loadClassBinaryName 方法进行类加载,如果找到了就返回对应的 class,否则返回 null,抛出错误(以上源码属于 Android - 9.0,每个版本略有不同,但思路是一样的)。

DexPathList

关于热修复

知道了类是怎么加载之后就可以进行热修复,这里仅提供一种思路。从上面说明已经知道类是通过遍历 dexElemets 这个数组进行加载,我们只需把修复的 dex 文件插到数组的最前面即可。**

在 PathClassLoader 中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得 dexElements 中的DexFile,查找到了Key.class则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。因此实际上,一种热修复实现可以将出现Bug的class单独的制作一份fix.dex文件(补丁包),然后在程序启动时,从服务器下载fix.dex保存到某个路径,再通过fix.dex的文件路径,用其创建 Element 对象,然后将这个 Element 对象插入到我们程序的类加载器 PathClassLoader 的 pathList 中的 dexElements 数组头部。这样在加载出现Bug的class时会优先加载fix.dex中的修复类,从而解决Bug。

热修复
而这一系类的操作都是通过反射来完成,包括生成 dex 文件,系统是怎么通过 ClassLoader 生成 dex,怎么生成 Elements 数组,只需按照它的步骤一步一步反射即可。
前面提到在 Android N 的情况下设备在闲置状态下会通过 AOT 转换成机器码,那在这种情况下,前面提到的这种的热修复手段就不再适用了,Tinker 对于这种情况的处理方法就是自定义一个 ClassLoader 来进行类加载。

任何ClassLoader子类,都可以重写 loadClass 与 findClass 。一般如果你不想使用双亲委托,则重写loadClass 修改其实现。而重写 findClass 则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class。

最后有一点需要注意的是热修复不仅仅是只用到了类加载,同时包括了 gradle 等其他方面的知识,这里只是指出了类加载在热修复中的应用。

感谢读到这里的你,希望对你有所帮助~

你可能感兴趣的:(关于Android-类加载)