Android 虚拟机与类加载机制
Android 应用程序运行在 Dalvik/Art 虚拟机上,并且每一个应用程序都有一个单独的 Dalvik/Art 虚拟机实例。
1、Dalvik 虚拟机
Android 应用程序运行在 Dalvik/Art 虚拟机上,并且每一个应用程序都有一个单独的 Dalvik/Art 虚拟机实例。
1.1 JVM 与 Dalvik
Dalvik 虚拟机也算是一个 Java 虚拟机,它是按照 JVM 虚拟机规范实现的,二者的特性差不多,不过还是有一些区别的:
- 执行的指令集不同:Java 虚拟机执行的是 class 文件,Dalvik 虚拟机执行的是 dex 文件
- Java 虚拟机的指令集基于堆栈,Dalvik 虚拟机的指令集基于寄存器
- JDK 1.8 默认垃圾回收器是 Parallel Scavenge(年轻代)+ ParallelOld(老年代),而 Dalvik 默认垃圾回收器是 CMS
DEX(Dalvik Executable Format)是专为 Dalvik 设计的一种压缩格式,它是很多 class 文件处理压缩后的产物,最终可以在 Android 运行时环境执行。
基于栈的虚拟机
基于栈的虚拟机会给每一个运行的线程分配一个独立的栈(虚拟机栈,是 JVM 运行时数据区的五大组成部分之一)。栈中记录了方法调用的历史,每有一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,代表当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。示意图如下:
对于一段很简单的代码,可以通过查看字节码文件查看它的指令:
指令含义:
- ICONST_1:将 int 类型常量 1 压入操作数栈位置 1
- ISTORE0:将栈顶 int 类型值存入局部变量表位置 0。test() 是一个静态方法,因此局部变量表就不用把位置 0 预留出来保存 this
- IADD:执行 int 类型的加法
这些指令的执行过程图如下:
基于寄存器的虚拟机
寄存器是 CPU 的组成部分,是有限存储容量的高速存储部件,它们可用来暂存指令、数据和地址。
上图是寄存器的简化结构,运行步骤如下:
- 从程序计数器指定的位置取出指令放到指令寄存器中
- 根据指令内容 LOADA,100 从内存地址 100 取出数据放到数据寄存器 AX 中
- 上一条指令执行完毕,程序计数器 +1,再从头循环上述过程。直到执行完 STOREC,108 将计算结果保存到内存地址 108 处
基于寄存器的虚拟机,实际上是为了模拟上述的工作流程,而不是真正的在物理上使用了 CPU 中的寄存器来完成上述工作的。
基于寄存器的虚拟机中没有操作数栈和局部变量表,但是有很多虚拟寄存器(理解成用寄存器代替了操作数栈和局部变量表吧)。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与 JVM 相似,在 DalvikVM 中每个线程都有自己的 PC 和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。
与 JVM 相比,可以发现 DalvikVM 的指令数明显减少了,数据移动次数也明显减少了(没有了在操作数栈和局部变量表之间的移动)。
1.2 ART 与 Dalvik
Dalvik 虚拟机执行的是 dex 字节码,解释执行。从 Android 2.2 版本开始,支持 JIT 即时编译(JustInTime)。JIT 是指在程序运行的过程中会选择热点代码(经常执行的代码)进行编译或者优化。
而 ART(Android Runtime)是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认 Android 运行时。ART 虚拟机执行的是本地机器码。Android 的运行时从 Dalvik 虚拟机替换成 ART 虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK 仍然是一个包含 dex 字节码的文件。
ART 虚拟机执行的本地机器码是安装的时候预编译产生的。
Dalvik 下安装应用时,会将 dex 字节码文件优化生成 odex 文件;ART 引入了预先编译机制(AheadOfTime),即在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex 中的字节码将被编译成本地机器码。进行 AOT 最恰当的时机也是在安装应用的时候。
通过上图可以看出两个虚拟机对 dex 文件不同的处理方式:
- Dalvik 虚拟机进行 dexopt 操作,在加载一个 dex 文件时,对 dex 文件进行验证和优化的操作,其对 dex 文件的优化结果变成了 odex(Optimizeddex)文件,这个文件和 dex 文件很像,只是使用了一些优化操作码。
- ART 虚拟机进行 dex2oat 操作,采用预先编译机制,在安装时对 dex 文件执行 AOT 提前编译操作,编译为 ART 可执行的 elf 文件(机器码)。
预先编译机制 AOT 是和即时编译 JIT 相对应的概念。
1.3 Art 虚拟机的优化
Android 从最初的版本使用的是 Dalvik 虚拟机,在 2.2 版本加入了 JIT。而 Art 虚拟机在 4.4 版本是一个开发者选项,从 5.0 版本开始作为默认使用的虚拟机。
由于 Art 虚拟机在安装应用时会使用 AOT 的预编译,因此 5.x 和 6.x 版本在安装应用时会比原来慢,为了解决这个问题又在 7.0 时做了改进,混合使用 AOT 编译、解释和 JIT:
- 最初安装应用时不进行任何 AOT 编译(安装又快了),运行过程中解释执行,对经常执行的方法进行 JIT,经过 JIT 编译的方法将会记录到 Profile 配置文件中
- 当设备闲置和充电时,编译守护进程会运行,根据 Profile 文件对常用代码进行 AOT 编译。待下次运行时(下一次启动时)直接使用
在读取 base.odex 文件执行代码时,会对经常执行的方法做 JIT 处理,放到 Profile 配置文件中,这是图中 collect 的过程;然后在设备闲置和充电时,通过 get profile 拿到这些配置文件,在 BackgroundDexOptService(这是一个 JobService)内运行 dex2oat 工具得到 base.art 文件。
当应用下次运行时,去看 /data/app 目录下是否有 base.art 这个文件,如果有,就用 ClassLoader 把这个文件中的类加载进内存,以后可以执行机器码了,这样就不用再去找 base.odex 文件以解释方式执行代码了。
2、ClassLoader
任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载器 ClassLoader。ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的:
class Class<T> { ... private transient ClassLoader classLoader; ... }
ClassLoader 是一个抽象类,而它在 Android 中的具体子类主要有:
- BootClassLoader:用于加载 Android Framework 层 class 文件。
- PathClassLoader:用于 Android 应用程序类加载器。可以加载指定的 dex,以及 jar、zip、apk 中的 classes.dex。
- DexClassLoader:用于加载指定的 dex,以及 jar、zip、apk 中的 classes.dex。
用代码进行一下测试:
Log.d(TAG, "Activity.class 由: " + Activity.class.getClassLoader() + " 加载");
Log.d(TAG, "MainActivity.class 由: " + MainActivity.class.getClassLoader() + " 加载");
Log.d(TAG, "String.class 由: " + String.class.getClassLoader() + " 加载");
输出为:
D/MainActivity: Activity.class 由: java.lang.BootClassLoader@5052f32 加载
D/MainActivity: MainActivity.class 由: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.myapplication-x6QX4W6vcSz6MxKY4vpW2w==/base.apk", dex file "/data/user/0/com.example.myapplication/app_fake_apk/app/classes.dex"],nativeLibraryDirectories=[/data/app/com.example.myapplication-x6QX4W6vcSz6MxKY4vpW2w==/lib/x86, /system/lib]]] 加载
D/MainActivity: String.class 由: java.lang.BootClassLoader@5052f32 加载
MainActivity 继承自 AppCompatActivity,而 AppCompatActivity 是官方提供的第三方库中的类,不算是系统文件,因此它是被 PathClassLoader 加载的。
一些博客里说 PathClassLoader 只能加载已安装的 apk 的 dex,其实这说的应该是在 Dalvik 虚拟机上,但现在一般不用关心 Dalvik 了。
2.1 PathClassLoader
ClassLoader 的任务就是把 class 文件读取成 byte[] 然后再转换成 Class 对象,像我们平时自己做的应用中的 class 文件都是通过 PathClassLoader 进行加载的,下面结合源码来看看具体是怎么做的。
loadClass()
类加载器都是通过 ClassLoader 的 loadClass() 去加载一个类的,那么去 PathClassLoader 中查看:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
PathClassLoader 中没有 loadClass(),去父类 BaseDexClassLoader 中找,也没有,再向上找到抽象父类 ClassLoader:
public abstract class ClassLoader {
// 当前ClassLoader对象的父ClassLoader
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1.找缓存,如果该类已经被加载过,直接从缓存中取。
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 2.调用父加载器(注意不是父类加载器)的loadClass()
c = parent.loadClass(name, false);
} else {
// 看源码就是直接 return null(与Java不同)。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 3.如果前面两次都没找到要加载的类,通过自己的findClass()再找一次。
c = findClass(name);
}
}
return c;
}
}
我们看到 loadClass() 主要做了三件事:
- 先通过 findLoadedClass() 去找缓存,看看 name 所对应的类是否在之前被加载过,如果是,就可以直接作为返回结果了,否则就继续向下执行。
- 倘若缓存未命中,先让自己的父加载器通过其 loadClass() 去加载这个类,如果加载成功也就直接作为返回结果,否则执行下一步。需要注意的是,父加载器并不是指父类,与当前 ClassLoader 并不存在继承关系。
- 上一步加载失败后,就只能通过当前 ClassLoader 的 findClass() 去加载 name 对应的类了。
关于第二点的父加载器我们再多说一点。在创建 PathClassLoader 对象时,其构造方法要求传入一个 ClassLoader,就是父加载器 parent:
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
这个 parent 的类型不必是 PathClassLoader 的父类,即父加载器类型不必是当前类加载器的父类。
这一点在源码中也有所印证,创建系统使用的 PathClassLoader 对象时,传的 parent 是一个 BootClassLoader 实例。在 ActivityThread 的 handleBindApplication() 进行 Application 绑定时,会获取 Application Context 的 ClassLoader:
private void handleBindApplication(AppBindData data) {
...
// Continue loading instrumentation.
if (ii != null) {
initInstrumentation(ii, data, appContext);
} else {
mInstrumentation = new Instrumentation();
mInstrumentation.basicInit(this);
}
...
}
private void initInstrumentation(
InstrumentationInfo ii, AppBindData data, ContextImpl appContext) {
...
final ContextImpl instrContext = ContextImpl.createAppContext(this, pi,
appContext.getOpPackageName());
try {
// 获取 App Context 的 ClassLoader
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate instrumentation "
+ data.instrumentationName + ": " + e.toString(), e);
}
...
}
ContextImpl 的 getClassLoader() 会根据条件获取 ClassLoader 对象:
@Override
public ClassLoader getClassLoader() {
return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
我们看保底的由系统提供的 ClassLoader:
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
// 创建 PathClassLoader,并指定其父加载器是 BootClassLoader
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
PathClassLoader 的父类是 BaseDexClassLoader,而系统为 PathClassLoader 指定的父加载器是 BootClassLoader。
双亲委托机制
回到 ClassLoader 的 loadClass(),可以看出对于任意一个 ClassLoader,它想要加载 class 文件时,都是先去找它的父加载器去 loadClass(),这就是我们常说的双亲委托机制
:
某个类加载器在加载类时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。
使用双亲委托机制的原因是:
- 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次,直接去父加载器的缓存中拿就可以了。
- 安全性考虑,防止核心 API 库被随意篡改。
对于第二点,需要解释一下。比如说,没有采取双亲委托机制,即代码变成这样:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 找缓存,如果该类已经被加载过,直接从缓存中取。
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
return c;
}
缓存中没有直接就在该 ClassLoader 对象中自己用 findClass() 去找这个类,那么假如我写一个跟系统全类名相同的类 java.lang.String,findClass() 在加载的时候就会加载到我们自己写的 java.lang.String 类,使得系统类被篡改,引发安全问题。
反过来说,使用了双亲委托,那么总是 BootClassLoader 先去加载系统里边的类,相当于加了一层拦截,将篡改系统代码的隐患给拦截掉了。
findClass()
最后看到 findClass(),它在抽象基类 ClassLoader 中是一个未实现的方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
所以就得看子类实现了,BaseDexClassLoader 重写了该方法:
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
// 实例化 pathList,传入了 dex 文件的路径 dexPath
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
...
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
// c 的判空处理,省略...
return c;
}
在看 DexPathList 的 findClass() 之前,先看它的构造方法,会实例化一个非常重要的成员变量 dexElements:
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java:
private Element[] dexElements;
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
}
// dexPath 是 dex 文件的路径,它可以是多个 dex 文件,形式为 /a/a\.dex;/a/b.dex
// 该方法会将 searchPath 中的所有文件路径分离,并创建出文件对象装入 List<File> 中。
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
List<File> result = new ArrayList<>();
if (searchPath != null) {
// File.pathSeparator是分号; 而File.separator是反斜杠\
for (String path : searchPath.split(File.pathSeparator)) {
...
result.add(new File(path));
}
}
return result;
}
// 遍历 List<File>,将 File 封装成 DexFile 后再封装成 Element,存入 Element[]
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;
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
// 将普通的 file 文件封装成 DexFile 对象
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
...
}
} else {
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);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
dexElements 是一个 Element 数组,而 Element 内封装着 DexFile 对象。然后我们再来看 DexPathList 的 findClass():
public Class<?> findClass(String name, List<Throwable> suppressed) {
// dexElements 是一个 Element[],查找类的工作会再次转交给 Element
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
...
return null;
}
static class Element {
private final DexFile dexFile;
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
// 最终是由 DexFile 的 loadClassBinaryName() 做类的加载
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
}
通过 Element 内封装的 DexFile 的 loadClassBinaryName() 调用 native 方法 defineClassNative() 完成类的加载:
/libcore/dalvik/src/main/java/dalvik/system/DexFile.java:
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
// native 方法调用的是 /art/runtime/native/dalvik_system_DexFile.cc 中的 DexFile_defineClassNative()
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile)
throws ClassNotFoundException, NoClassDefFoundError;
整个流程下来,大致是 ClassLoader 持有一个 DexPathList 对象,DexPathList 内维护着一个 Element[],每个 Element 内都封装着一个 DexFile 可以用来加载对应的 dex 文件。时序图如下:
2.2 DexClassLoader
PathClassLoader 与 DexClassLoader 具有共同父类 BaseDexClassLoader:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
可以看到两者唯一的区别在于:创建 DexClassLoader 需要传递一个 optimizedDirectory 参数,并且会将其创建为 File 对象传给 super,而 PathClassLoader 则直接给该参数传 null。因此两者都可以加载指定的 dex,以及 jar、zip、apk 中的 classes.dex:
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex", dexOutputDir.getAbsolutePath(), null, getClassLoader());
其实,optimizedDirectory 参数就是 dexopt 产出 odex 的目录。那 PathClassLoader 创建时,这个目录为 null,是否意味着不进行 dexopt?并不是,optimizedDirectory 为 null 时的默认路径为:/data/dalvik-cache。
在 API 26 源码中,将 DexClassLoader 的 optimizedDirectory 标记为了 deprecated 弃用,实现也变为了:
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
和 PathClassLoader 别无二致。
3、热修复原理简述
ClassLoader 的一个典型应用是热修复,简单说下原理。
首先基于 ClassLoader 加载类过程的分析,找该 ClassLoader 缓存 -> 让父 ClassLoader 加载 -> 该 ClassLoader 自己加载 dex 文件,如果想要实现热修复,那么前两步是无法实现的,也就是说你需要在第一次加载 dex 文件时就把需要做热修复的 dex 文件(以下称补丁 dex)加载进内存。
而在如何加载补丁 dex 的问题上,可以先看下图:
由于使用双亲委托机制加载类,两个全类名相同的类,先被加载的那个会进入内存变成 Class 对象,而后被遍历到的 dex 文件中的类就不会被加载。
再加上 ClassLoader 遍历 Element 数组时是按照数组角标顺序由小到大遍历的,那么我们可以通过反射,把带有需要热修复的类的 Patch.dex 文件放到 Element 数组的最前面,然后让 ClassLoader 先加载 Patch.dex 中的类,就可以实现热修复了。
以上是实现思路,实现过程可以这样安排:
- 获取到当前应用的 PathClassLoader
- 反射获取到 DexPathList 的成员 pathList
- 反射修改 pathList 的 dexElements:
- 把补丁包的 patch.dex 转化为 Element[]
- 获得 pathList 的 dexElements 属性
- patch+old 合并,并反射赋值给 pathList 的 dexElements
如果对热修复感兴趣可以参考这篇文章 Android 热修复。
参考资料:
Android 9.0 ART编译分析(二)-Installd触发dex2oat编译流程
系统ClassLoader相关及Application初始化简单分析及总结
更多推荐
所有评论(0)