点赞关注,不再迷路,你的支持对我意义重大!
🔥 Hi,我是丑丑。本文「Android 路线」| 导读 —— 从零到无穷大 已收录。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
目录
前置知识
1. Java 类加载的委派模型
Java 类加载是一种委托机制(parent delegate),即:除了顶级启动类加载器(bootstrap classloader)之外,每个类加载器都有一个关联的上级类加载器(parent 字段)。当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载。
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 libPaths) {
pathList.addNativePath(libPaths);
}
}
可以看到,BaseDexClassLoader 将 findClass() 的任务委派给 DexPathList 对象处理,这个 DexPathList 指定了搜索类和 so 动态库的路径。
【todo】
2.3 PathClassLoader & DexClassLoader 类加载器
PathClassLoader & DexClassLoader 是 BaseDexClassLoader 的子类,从源码可以看出,它们其实都没有重写方法,所以主要的逻辑还是在 BaseDexClassLoader。 并且它们只在 Android 9.0 之前有区别:
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
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 否则继续以解释方式执行方法;
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 编译。
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 编译混合的运行方式。
参考资料
《调试 ART 垃圾回收》 —— Android Developers
《深入理解Java虚拟机(第3版本)》(第10、11章)—— 周志明 著
《深入理解Android:Java虚拟机 ART》 —— 邓凡平 著
《深入理解 JVM 字节码》(第4、5章)—— 张亚 著
创作不易,你的「三连」是丑丑最大的动力,我们下次见!