目录
前置知识
- Java 类加载:Java 虚拟机 | 类加载机制
- Java 编译:Java | 编译过程(编译前端 & 编译后端)
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 文件中加载类。
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); } 复制代码
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 编译混合的运行方式。