一.Android虚拟机
1.初代虚拟机Dalvik
与JVM的不同?
①Dalvik执行dex文件
DVM也是实现了JVM规范的一个虚拟器,默认使用CMS垃圾回收器(这篇文章中有讲解CMS),但是与JVM运行 Class 字节码不同,DVM执行的是Dex文件。
那么class文件和dex文件的区别是什么呢?
可以这么说:
- 对于class:one File,one Class
- 对于dex:one File,many classes
②DVM的指令集是基于寄存器的
除了处理的文件不同,还有一点不同是:DVM的指令集是基于寄存器的,JVM的指令集是基于堆栈的。
说的清楚点,就是把JVM的局部变量表和操作数栈合并为了一个虚拟寄存器。如图
同样的一段程序


上图是JVM,基于堆栈。下图是DVM,基于寄存器

与JVM相比,DVM程序的指令数明显减少,数据移动次数也明显减少了。
其他的地方和JVM,基本相同,比如一个线程有一个栈,一个方法是一个栈帧等等。
2.ART
ART是Dalvik的升级版。在了解两者的区别之前,我先详细介绍下Dalvik
①详细介绍下Dalvik
Dalvik虚拟机执行的是dex字节码,解释执行,从Android2.2开始支持JIT即时编译。
JIT是什么意思呢?可以这么理解,它会找到一次程序执行时执行次数比较多的代码(比如循环),将其直接翻译成机器码,供本手机直接执行,而不用再通过字节码交给虚拟机执行了。我画了一幅图,可以帮助理解(图中Dalvid应该是Dalvik,在这里纠正一下)

值得一提的是。JIT只是这次运行才有效,下一次执行就另外一回事,所以这也是Android N的虚拟机产生的原因之一
以上是对Dalvik的介绍
②详细介绍下ART
ART是Android5.0之后的默认虚拟机。它执行的直接就是本地机器码,而不是dex文件。这个机器码是程序(APK文件)安装的时候翻译出来的。也就是预先编译机制(AOT),与JIT机制对应的。所以在那个时期,应用的安装都比较慢。
Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件。而ART下将应用的dex字节码翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候。ART引入了预先编译机制(Ahead Of Time),在安装时,ART使用设备自带的dex2oat 工具来编译应用,dex中的字节码将被编译成本地机器码。

3.Android N的运作方式
从Android N之后,虚拟机又变了,它将解释执行,JIT,AOT编译混合使用了。具体是什么情况呢?
- ①最初安装应用时不进行任何AOT编译(安装又快了),运行过程中解释执行,对经常执行的方法进行JIT,经过JIT编译的方法将会记录到Profile配置文件中。
- ②当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行AOT编译。待下次运行时直接使用。
那么有人会问了,JIT不是都编译了吗,为啥AOT还要编译一次呢?我认为有两个原因,一个是JIT不能持久化,只是针对一次程序运行。再一个是记录到配置文件中的是方法的信息,而不是方法本身。我认为在AOT编译之前要根据这些信息找到这些类,然后再编译成机器码。
二.Android类加载器
1.介绍
任何一个Java程序都是由一个或多个class文件组成,在程序运行时,需要将class文件加载到JVM中才可以使用,负责加载这些class文件的就是Java的类加载机制。ClassLoader的作用简单来说就是加载 class文件,提供给程序运行时使用。每个Class对象的内部都有一个classLoader字段来标识自己是由哪个ClassLoader加载的。Android中也有类加载机制,作用和Java中的是基本一样的。
2.Android中的类加载器
有几个比较重要的类。分别是
-
BootclassLoader 用于加载Android Framework层class文件。比如String,Activity类的加载 -
PathclassLoader 用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex。我们自己写的类一般都是被这个加载器加载的。 -
DexclassLoader 用于加载指定的dex,以及jar、zip、apk中的classes.dex
他们之间的关系是这样的

3.双亲委托机制
让我们看一下一个类是怎么被加载的。我们直接进入PathClassLoader的loadClass方法看
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}

①首先从缓存里面找
看看这个类是否已经加载过了。因为类加载需要IO等一些比较耗时的操作,所以能从缓存中加载就从缓存中找。
②如果缓存中没有,就从父类加载器中找。

需要注意的一点是,此父非彼父,之前我们说的父类是继承关系的父类,也就是PathClassLoader的父类BaseDexClassLoader。但是这里的parent不是BaseDexClassLoader对象,如图,是parent的定义

这个parent是父类加载器,是BootClassLoader。在PathClassLoader构建的时候有一个构造方法,传进来BootClassLoader对象,然后给parent赋的值。
如图

- 这就好比是父亲和师傅。自己的身体是父亲给的,但是拜师学艺后基本与父亲断了联系,遇到问题先找师傅,师傅也如同父亲一样。遇到问题先问问师傅有没有遇到过,如果师傅可以处理的话就不用自己处理了,如果师傅不能处理,就得靠自己了。
具体到这里的程序就是遇到一个类,先问问师傅(parent)有没有加载过,或者师傅能不能加载了,如果师傅能加载就不用本人加载,如果师傅做不到就自己出马。(这里,本人是PathClassLoader,父亲是其父类BaseDexClassLoader,师傅是BootClassLoader,BootClassLoader的对象在PathClassLoader中的命名是parent。)
先看看parent是否可以加载,不行的话再自己加载。
这就是双亲委托机制。
双亲委托机制的好处?
- 1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子
ClassLoader再加载一次。
比如String这个类,像这种核心类,父类加载器也就是BootClassLoader已经加载过了,PathClassLoader就没必要再加载一次了。 - 2、安全性考虑,防止核心API库被随意篡改。
比如我们自己也弄一个String类,它属于我们自己定义的java的lang包下,而不是JDK里面的,如图

如果我们有双亲委托机制的话,就会加载JDK里面的String,能正常使用,如果没有双亲委托机制,那么我们就会用自己定义的这个String来替换掉系统的String,也就是核心API库被随意篡改了。
OK,让我们继续。
③父类加载器不行,就自己加载
自己加载就是执行findClass方法,这里的findClass方法在PathClassLoader的父类中有,根据多态,也是执行的父类的findClass方法,让我们进入看一下

继续找pathList对象,定义在这里

那么这个DexPathList是啥呢?进入其构造方法中看一下

啥也不管,就看红框的部分。发现它给dexElements成员赋了值
值得一提的是splitDexPath方法。dexPath可能传入的是/a/a.dex;/a/b.dex,splitDexPath方法就是把这个字符串以分号隔开,分成一个List集合,本例就是分成含/a/a.dex和/a/b.dex两个元素的List集合,可以理解为一个元素就是一个dex文件
dexElements是一个数组,也可以理解为一个元素就是一个dex文件

我们知道了DexPathList是啥(主要是里面的dexElements数组),然后再看它的findClass方法,因为是调用了它的findClass方法

发现它是从Element数组里面一个一个地取出dex(for循环),然后完成加载。
用张图总结就是

PathClassLoader中没有实现findClass方法,所以从其父类BaseDexClassLoader中找findClass方法,发现它调用了DexPathList类的findClass方法,所以进去看DexPathList类的findClass方法,发现它是从dexElements数组中以循环的形式一个一个地取出dex文件然后进行加载。dexElements是在DexPathList对象构建的时候就已经赋值了(也就是在构造方法中)
三.热修复原理
学习了上面的知识后,就很容易明白热修复的原理了。
比如我的某一个类出bug了

那么我可以写出更改后的class文件,将其打包成dex文件作为补丁包,如图

然后再把它插到dexElements数组的最前面,如图

这样的话,系统会先加载前面的Demo1.class,等到后面加载到有Bug的Demo1.class的时候,会发现前面已经加载过一次Demo1.class了,所以就不会加载这次有Bug的Demo1.class了,这就完成了热修复。










