Android虚拟机
Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。
Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基
本寄存器的,而后者的指令集是基于堆栈的。
那么什么是基于栈的虚拟机,什么又是基于寄存器的虚拟机?
基于栈的虚拟机
对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每有
一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,其代表着当前执行的方法。基于栈的
虚拟机通过操作数栈进行所有操作。
字节码指令
ICONST_1
: 将int类型常量1压入操作数
栈;
ISTORE 0
: 将栈顶int类型值存入局部变
量0;
IADD
: 执行int类型的加法 ;
执行过程
基于寄存器的虚拟机
基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。
基于栈和寄存器的虚拟机对比
栈式VS寄存器式 | 相比 |
---|---|
指令条数 | 栈式 > 寄存器式 |
代码尺寸 | 栈式 < 寄存器式 |
移植性 | 栈式优于寄存器式 |
指令优化 | 栈式更不易优化 |
解释器执行速度 | 栈式解释器速度稍慢 |
代码生成速度 | 栈式简单 |
简易实现中的数据移动次数 | 栈式移动次数多 |
和JVM版相比,可以发现Dalvik版程序的指令明显减少了,数据移动也明显减少了。
分类 | 特点 | 优点 | 缺点 |
---|---|---|---|
基于栈的虚拟机 | 指令集主要操作的是栈。指令通常不直接操作存储器或寄存器,而是操作栈顶的元素 | 指令集可以简单且与硬件无关 | 访问局部变量和传递参数可能需要频繁的入栈和出栈操作,这可能影响性能 |
基于寄存器的虚拟机 | 指令集主要操作的是寄存器。指令通常会指定操作数所在的寄存器 | 可以减少访问局部变量和传递参数所需的指令数量,从而提高性能 | 指令集可能更复杂,且与硬件更相关 |
ART与Dalvik
DVM也是实现了JVM规范的一个虚拟机,默认使用CMS垃圾回收器,但是与JVM运行Class字节码不同,DVM执行dex(Dalvik Executable Format)—专为Dalvik设计的一种压缩格式.Dex文件是很多.class文件处理压缩后的产物,最终可以在Android运行时环境执行.
从Android2.2版本开始,支持JIT即时编译(Just In Time).在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化.
而**ART(Android Runtim)**是在Android4.4中引入的一个开发者选项,也是Android5.0及更高版本的默认Android运行时.ART虚拟机执行的是本地机器码.Android的运行时从Davik虚拟替换成ART虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含APK的字节码文件.
ART和Dalvik都是运行Dex字节码的兼容运行时,因此针对Dalvik开发的应用也能在ART环境中运行
那么,ART虚拟机执行的本地机器码从哪里来?
dexopt和dex2oat
dexopt和dex2oat都是Android系统中用于优化和转换Dex文件的工具,它们在应用安装和运行过程中起着重要的作用。
dexopt
在Dalvik中虚拟机在加载一个dex文件时,对dex文件进行验证和优化的操作,其对dex文件的优化结果变成了odex(Optimized dex)文件,这个文件和dex文件很像,只是使用了一些优化操作码
dex2oat
ART 预先编译机制,在安装时对dex文件执行AOT提前编译操作,编译为OAT(实际上是ELF文件)可执行文件(机器码)
Davik下应用在安装的过程中,会执行一次优化,将dex字节码进行优化生成odex文件.而ART下应用的dex字节码能翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候.ART引入了预先编译机制(Ahead Of Time),在安装时,ART使用设备自带的dex2oat
工具来编译应用,dex中的字节码被编译成本地机器码.
Android N的运行方式
-
从Android 7.0(Nougat)开始,Android系统引入了一种新的运行方式,称为混合编译(Hybrid Compilation)。这种方式结合了之前Android Runtime (ART) 的Ahead-of-Time (AOT) 编译和Just-in-Time (JIT) 编译的优点。
-
在Android N及以后的版本中,应用在安装时不再进行全量的AOT编译,而是生成更小的OAT文件,这个过程称为快速安装。这样可以减少应用的安装时间,减少存储空间的占用,同时也减少了系统的启动时间。
-
然后,当应用运行时,Android系统会使用JIT编译器对热点代码进行JIT编译,即在运行时将字节码编译成机器码。这样可以提高应用的运行效率,因为JIT编译器可以根据运行时的信息进行更优的优化。
-
最后,当设备处于空闲状态时,Android系统会启动后台编译服务,对JIT编译过的代码进行AOT编译,生成更优化的机器码。这个过程称为后台编译,或者Profile-guided Compilation。这样可以进一步提高应用的运行效率,同时也减少了运行时的CPU和内存占用。
总的来说,Android N的运行方式结合了AOT编译和JIT编译的优点,既保证了应用的运行效率,又减少了系统的启动时间和存储空间的占用。
类加载机制
概述
在Android系统中,类加载机制主要由三种类加载器实现:BootClassLoader、PathClassLoader和DexClassLoader。
- BootClassLoader:BootClassLoader是Android系统的启动类加载器,它负责加载Android系统的核心类库,例如android.*和java.*等包中的类。BootClassLoader在Android系统启动时由Zygote进程创建,它是所有ClassLoader的父加载器。BootClassLoader的加载路径是固定的,通常包括/system/framework等目录。
- PathClassLoader:PathClassLoader主要用于加载Android应用的主dex文件和系统类库。它是BootClassLoader的子类,它的父加载器是BootClassLoader。PathClassLoader不能加载文件系统上的任意位置的.dex文件或.apk文件。
- DexClassLoader:DexClassLoader可以加载文件系统上的任意位置的.dex文件、.jar文件、.apk文件和.zip文件(包含.dex文件)。它也是BootClassLoader的子类,它的父加载器是BootClassLoader。DexClassLoader通常用于实现插件化技术,可以在运行时动态加载和卸载代码。
这三个类加载器在加载类时,都会遵循双亲委派模型。当一个类加载器需要加载一个类时,它首先会请求其父类加载器来加载这个类,只有当父类加载器无法加载这个类时,它才会尝试自己加载这个类。
在Android 5.0及以后的版本中,由于引入了ART运行环境,类加载机制发生了一些变化。在应用安装时,dex2oat工具会将.dex文件编译成.oat文件,然后在运行时直接加载.oat文件。这样可以提高应用的运行效率,但需要更多的存储空间来存储.oat文件。
总的来说,Android中的类加载机制主要由BootClassLoader、PathClassLoader和DexClassLoader实现,它们可以加载Android系统的核心类库和应用的.dex文件。
ClassLoader
任何一个Java程序都是由一个或多个class文件组成,在程序运行时,需要将class文件加载到JVM中才可以使用,负责加载这些class文件的就是Java的类加载机制.ClassLoader的作用简单来说就是加载class文件,提供给程序运行时使用.每个Class对象的内部都有一个classLoader
字段来标识自己是由哪个ClassLoader加载的
class Class<T> {
...
private transient ClassLoader classLoader;
...
}
ClassLoader是一个抽象类,而它的具体实现类主要有:
-
BootClassLoader
用于加载Android Frameword层class文件
-
PathClassLoader
用于Android应用程序类加载器,可以加载指定的dex,以及jar,zip
,apk中的classes.dex
-
DexClassLoader
用于加载指定的dex,以及jar,zip,apk中的classes.dex
它们之间的关系是:
PathClassLoader
与DexClassLoader
的共同父类是BaseDexClassLoader
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
可以看到两者唯一的区别在于:创建DexClassLoader
需要传递一个optimizedDirectory
参数,并且会将其创建为File
对象,而PathClassLoader
则直接给到null,因此两者都可以加载指定的dex,以及jar
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
在API26源码中,将DexCLassLoader的optimizedDirectory标记为了deprecated弃用,实现也变为了:
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); }
和PathClassLoader一摸一样了
双亲委托机制
在创建ClassLoader
时需要接收一个ClassLoader parent
参数,这个parent
的目的就在于实现类加载的双亲委托机制.
某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载.
protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundExecption {
// 检查class时候有被加载
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClann(name, false);
} else {
// parent为null,则调用BootClassLoader进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果都找不到就自己查找
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
双亲委托机制的好处:
- 避免重复加载,当父类加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次.
- 安全性考虑,防止核心API库被随意篡改.
findClass
可以看到所有父ClassLoader无法加载Class时,则会调用自己的findClass
方法,findClass
在ClassLoader中的定义为:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundExecption(name);
}
其实任何ClassLoader子类,都可以重写classLoader
与findClass
.一般如果你不想使用双亲委托,则重写loadClass
修改其实现.而重写findClass
则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class,而我们的PathClassLoader
会自己负责加载MainActivity
这样的程序中自己编写的类,利用双亲委托父ClassLoader加载Framework中的Activity.说明PathClassLoader
并没有重写loadCLass
,因此我们可以来看看PathClassLoader中的findClass
是如何实现的
public class BaseDexClassLoader extends ClassLoader {
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<>();
// 查找特定的class
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;
}
}
实现非常简单,从pathList
中查找class.继续查看DexPathList
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
// ......
// splitDexPath 实现为返回 List<File>.add(dexPath)
// spiltDexPath 实现为返回 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
this.dexElements = makeDexElements(spiltDexPath(dexPath), optimizedDirectory,
suppressedExecptions, definingContext);
// .....
}
public Class findClass(String name, List<Throwable> suppressed) {
// 从element中获得Dex的 DexFile
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
// 查找class
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementSuppressedExceptions));
}
return null;
}
热修复
热修复(Hot-Fix)是一种在不需要重新安装应用的情况下修复应用程序错误的技术。在Android中,热修复通常通过动态加载技术实现,可以在应用运行时动态替换掉有问题的代码。
热修复的基本原理是使用DexClassLoader或PathClassLoader加载包含修复代码的.dex文件或.apk文件,然后通过反射或其他方式调用这些修复代码,从而替换掉有问题的代码。
PathClassLoader
中存在一个Element数组,Element类中存在一个dexFile成员表示dex文件,即:APK中有X个dex,则Element数组就有X个元素
在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。