Android | 从类加载到程序执行

3,907 阅读8分钟

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

目录


前置知识


1. Java 类加载的委派模型

Java 类加载是一种委托机制(parent delegate),即:除了顶级启动类加载器(bootstrap classloader)之外,每个类加载器都有一个关联的上级类加载器(parent 字段)。当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载。

更多内容:类加载: Java 虚拟机 | 类加载机制


2. Android 中的类加载器

在 Java 中,JVM 加载的是 .class 文件,而在 Android 中,Dalvik 和 ART 加载的是 dex 文件。这里的 dex 文件不仅仅指 .dex 后缀的文件,而是指携带 classed.dex 项的任何文件(例如:jar / zip / apk)。

这一节我们就来分析 Android ART 虚拟机 中的类加载器:

ClassLoader 实现类作用
BootClassLoader加载 SDK 中的类
PathClassLoader加载应用程序的类
DexClassLoader加载指定的类

2.1 BootClassLoader 类加载器

在 Java / Android 中,BootClassLoader 是委托模型中的顶级加载器,作为委托链的最后一个成员,它总是最先尝试加载类的。

  • 1、BootClassLoader 是单例的,一个进程只会有一个 BootClassLoader 对象,并在 JVM 启动的时候启动;
  • 2、BootClassLoader 的 parent 字段为空,没有上级类加载器(可以通过判断一个 ClassLoader#getParent() 是否来空来判断是否为 BootClassLoader);
  • 3、BootClassLoader#findClass(),最终调用 native 方法。

BootClassLoader 是 ClassLoader 的非静态内部类,源码如下:

ClassLoader.java

class BootClassLoader extends ClassLoader {

    public static synchronized BootClassLoader getInstance() {
        单例
    }

    public BootClassLoader() {
        没有上级类加载器,parent 为 null
        super(null);
    }

    @Override
    protected Class<?> findClass(String name) {
        注意 ClassLoader 参数:传递 null
        return Class.classForName(name, false, null);
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        1、检查是否加载过
        Class<?> clazz = findLoadedClass(className);

        2、尝试加载
        if (clazz == null) {
            clazz = findClass(className);
        }
        return clazz;
    }
}

-------------------------------------------------
Class.java
static native Class<?> classForName(String className, boolean shouldInitialize, ClassLoader classLoader) 

2.2 BaseDexClassLoader 类加载器

在 Android 中,Java 代码的编译产物是 dex 格式字节码,所以 Android 系统提供了 BaseDexClassLoader 类加载器,用于从 dex 文件中加载类。

BaseDexClassLoader.java

public class BaseDexClassLoader extends ClassLoader {

    private final DexPathList pathList;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        从 DexPathList 的路径中加载类
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            throw new ClassNotFoundException(...);
        }
        return c;
    }

    添加 dex 路径
    public void addDexPath(String dexPath, boolean isTrusted) {
        pathList.addDexPath(dexPath, isTrusted);
    }

    添加 so 动态库路径
    public void addNativePath(Collection<String> libPaths) {
        pathList.addNativePath(libPaths);
    }
}

可以看到,BaseDexClassLoader 将 findClass() 的任务委派给 DexPathList 对象处理,这个 DexPathList 指定了搜索类和 so 动态库的路径。

【todo】

2.3 PathClassLoader & DexClassLoader 类加载器

PathClassLoader & DexClassLoader 是 BaseDexClassLoader 的子类,从源码可以看出,它们其实都没有重写方法,所以主要的逻辑还是在 BaseDexClassLoader。 并且它们只在 Android 9.0 之前有区别:

DexClassLoader.java - Android 8.0

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

DexClassLoader.java - Android 9.0

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

PathClassLoader.java

public PathClassLoader(String dexPath,  String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}
参数描述
dexPath加载 dex 文件的路径
optimizedDirectory加载 odex 文件的路径(优化后的 dex 文件)
librarySearchPath加载 so 库文件的路径
parent上级类加载器

可以看到,在 Android 9.0 之前,DexClassLoader 的构造方法需要传入optimizedDirectory参数。不过从 Android 9.0 开始,DexClassLoader 也不需要传这个参数了,所以 Android 9.0 开始两个类就完全一样了。


3. 程序的执行:编译 & 解释

程序员通过源码的形式编写程序,而 CPU 只能识别 / 运行本地代码。将源码转换为本地代码有两种做法:解释和编译。

  • 解释: 通过解释器边翻译边执行,多次执行同一份代码需要重复解释翻译,效率低,但移植性更好;

  • 编译: 通过编译器将源程序完整的地翻译为本地代码,编译一次得到的产物可以反复执行,效率较高,但编译耗时。

“编译” 这个词在狭义和广义上有不同的理解,狭义上的编译是指将 .java 文件转换为*.class 文件或 .dex 文件的过程,也称为 编译前端。而广义的编译还包括运行期即时编译(JIT,Just in Time Compile)或者(静态的)提前编译(AOT,Ahead of Time Compile),这两种编译称为 编译后端

Java 没有采用极端的完全解释执行或者编译执行,而是采用了介于两者之间的执行方式。无论是 .class 文件还是 .dex 文件,都只是编译过程的中间产物,并没有完全编译为本地代码。在运行时,还需要虚拟机进行解释执行或者进一步编译。

下面,我们来讨论 Dalvik 和 ART 虚拟机上的程序执行。


4. Dalvik

4.1 Dalvik 上的 JIT

在 Dalvik 的早期的版本中是只有解释器的,同一份代码需要重复解释翻译多次,效率低,为了优化这个问题。从 Android 2.2 版本开始加入了 JIT 编译器,JIT 在运行时编译生成本地代码,就不用重复解释翻译,这样就加快了执行的速度。

虽然 JIT 编译可以提高代码执行速度,但是编译本身是耗时的事情,所以只应该对 “热点” 代码进行编译。那么即时编译器是如何探测热点代码的呢?主要有两种:基于采样 & 基于计数器

Dalvik 中的 JIT 采用的是基于计数器的热点探测,主要流程如下:

  • 0、设定一个“热门”代码的阈值;
  • 1、检查是否存在编译后的本地代码?有则执行;
  • 2、否则,记录代码的执行次数,每次执行时都比对一下看看有没有到阈值?
    • 2.1 是则向编译器发送即时编译请求,并以解释方式执行方法;
    • 2.2 否则继续以解释方式执行方法;

—— 引用自 paul.pub/android-dal… 强波(华为)著

4.2 dexopt 优化

在 Dalvik 虚拟机中,应用安装时会执行 dexopt 优化。这个过程主要是将 apk 中的 .dex 文件优化为 odex(optimized dex) 文件,保存在data/dalvik-cache目录,并将原来 apk 中的 .dex 文件删除。这样做的优点主要是:

  • 1、优化了 dex 文件;
  • 2、预先从 apk 中提取出 .dex 文件,启动速度略有加快。

5. ART

从 Android 4.4 开始,Android 系统就集成了 ART 虚拟机,不过默认是没有启用的,需要在开发者选项中手动开启。从 Android 5.0 开始,ART 虚拟机才被正式启用。

5.1 ART 上的 AOT(Android L 5.0)

在 ART 虚拟机中,应用安装时会执行 AOT 编译。即在程序运行之前提前使用 dex2oat 工具将 apk 中的 .dex 文件变化为 OAT 文件。OAT 文件遵循 ELF 格式,是 Unix 系统上的可执行文件。程序运行的时候就可以直接执行已经编译好的代码,相当于使用 AOT 编译提前预热。

—— 图片引用自网络

5.2 JIT 的回归(Android N 7.0)

AOT 编译虽然可以提前编译出本地代码,但是单纯的 AOT 编译会存在两种情况下用户等待时间过长的问题:

  • 1、应用安装时间过长;
  • 2、系统版本升级时,所有应用需要重新 AOT 编译。

—— 图片引用自网络

为了解决用户等待时间过长的问题,从 Android N 7.0 开始,Android 重新引入了 JIT,采用了 AOT 编译、解释和 JIT 编译混合的运行方式。主要工作流程如下:

  • 1、在应用安装时,不再进行 AOT 编译(安装速度变快了);
  • 2、在程序执行时,使用解释执行 + JIT 编译的方式,并且将经过 JIT 编译的热点方法记录到 profile 配置文件中;
  • 3、在设备闲置时,编译守护进程根据 profile 文件的记录的热点代码进行 AOT 编译。

—— 引用自 paul.pub/android-dal… 强波(华为)著


6. 总结

  • 1、Java 类加载是一种委托机制,当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载;

  • 2、JVM 加载的是 .class 文件,而 Dalvik 和 ART 加载的是 dex 文件,在 Android 中的类加载器主要是 BootClassLoader & PathClassLoader & DexClassLoader;

  • 3、将源码转换为本地代码有两种做法:解释和编译。解释是边翻译边执行,多次执行同一份代码需要重复解释翻译,效率低,但移植性更好; 编译是将源程序翻译为本地代码,编译一次得到的产物可以反复执行,效率较高,但编译耗时。

  • 4、Dalvik 从 Android 2.2 开始采用 JIT 编译,Dalvik 还会使用 dexopt 将 dex 文件优化为 odex 文件;

  • 5、ART 从 Android 5.0 正式启用,采用了 AOT 编译生成 oat 文件,存在安装 / 系统升级时用户等待时间过程的副作用。从 Android 7.0 开始,Android 重新引入了 JIT,采用了 AOT 编译、解释和 JIT 编译混合的运行方式。


参考资料


创作不易,你的「三连」是丑丑最大的动力,我们下次见!