一.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的局部变量表和操作数栈合并为了一个虚拟寄存器。如图

同样的一段程序

Android虚拟机与类加载机制_android

Android虚拟机与类加载机制_加载_02


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

Android虚拟机与类加载机制_android_03


与JVM相比,DVM程序的指令数明显减少,数据移动次数也明显减少了。

其他的地方和JVM,基本相同,比如一个线程有一个栈,一个方法是一个栈帧等等。

2.ART

ART是Dalvik的升级版。在了解两者的区别之前,我先详细介绍下Dalvik

①详细介绍下Dalvik

Dalvik虚拟机执行的是dex字节码,解释执行,从Android2.2开始支持JIT即时编译。

JIT是什么意思呢?可以这么理解,它会找到一次程序执行时执行次数比较多的代码(比如循环),将其直接翻译成机器码,供本手机直接执行,而不用再通过字节码交给虚拟机执行了。我画了一幅图,可以帮助理解(图中Dalvid应该是Dalvik,在这里纠正一下)

Android虚拟机与类加载机制_java_04


值得一提的是。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中的字节码将被编译成本地机器码。

Android虚拟机与类加载机制_加载_05

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

他们之间的关系是这样的

Android虚拟机与类加载机制_android_06

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;
}

Android虚拟机与类加载机制_java_07

①首先从缓存里面找

看看这个类是否已经加载过了。因为类加载需要IO等一些比较耗时的操作,所以能从缓存中加载就从缓存中找。

②如果缓存中没有,就从父类加载器中找。

Android虚拟机与类加载机制_class_08

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

Android虚拟机与类加载机制_android_09


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

如图

Android虚拟机与类加载机制_android_10

  • 这就好比是父亲和师傅。自己的身体是父亲给的,但是拜师学艺后基本与父亲断了联系,遇到问题先找师傅,师傅也如同父亲一样。遇到问题先问问师傅有没有遇到过,如果师傅可以处理的话就不用自己处理了,如果师傅不能处理,就得靠自己了。

具体到这里的程序就是遇到一个类,先问问师傅(​​parent​​​)有没有加载过,或者师傅能不能加载了,如果师傅能加载就不用本人加载,如果师傅做不到就自己出马。(这里,本人是​​PathClassLoader​​​,父亲是其父类​​BaseDexClassLoader​​​,师傅是​​BootClassLoader​​​,​​BootClassLoader​​​的对象在​​PathClassLoader​​​中的命名是​​parent​​​。)
先看看parent是否可以加载,不行的话再自己加载。

这就是双亲委托机制。


双亲委托机制的好处?
  • 1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子​​ClassLoader​​​再加载一次。
    比如​​​String​​​这个类,像这种核心类,父类加载器也就是​​BootClassLoader​​​已经加载过了,​​PathClassLoader​​就没必要再加载一次了。
  • 2、安全性考虑,防止核心API库被随意篡改。
    比如我们自己也弄一个​​​String​​​类,它属于我们自己定义的​​java​​​的​​lang​​包下,而不是JDK里面的,如图
  • Android虚拟机与类加载机制_加载_11

如果我们有双亲委托机制的话,就会加载JDK里面的​​String​​​,能正常使用,如果没有双亲委托机制,那么我们就会用自己定义的这个​​String​​​来替换掉系统的​​String​​,也就是核心API库被随意篡改了


OK,让我们继续。

③父类加载器不行,就自己加载

自己加载就是执行​​findClass​​​方法,这里的​​findClass​​​方法在​​PathClassLoader​​​的父类中有,根据多态,也是执行的父类的​​findClass​​方法,让我们进入看一下

Android虚拟机与类加载机制_jvm_12


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

Android虚拟机与类加载机制_class_13


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

Android虚拟机与类加载机制_android_14


啥也不管,就看红框的部分。发现它给​​dexElements​​成员赋了值

值得一提的是​​splitDexPath​​​方法。​​dexPath​​​可能传入的是​​/a/a.dex;/a/b.dex​​​,​​splitDexPath​​​方法就是把这个字符串以分号隔开,分成一个​​List​​​集合,本例就是分成含​​/a/a.dex​​​和​​/a/b.dex​​​两个元素的​​List​​​集合,可以理解为一个元素就是一个​​dex​​​文件
​​​dexElements​​​是一个数组,也可以理解为一个元素就是一个​​dex​​文件

Android虚拟机与类加载机制_加载_15

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

Android虚拟机与类加载机制_java_16


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

用张图总结就是

Android虚拟机与类加载机制_android_17


​PathClassLoader​​​中没有实现​​findClass​​​方法,所以从其父类​​BaseDexClassLoader​​​中找​​findClass​​​方法,发现它调用了​​DexPathList​​​类的​​findClass​​​方法,所以进去看​​DexPathList​​​类的​​findClass​​​方法,发现它是从​​dexElements​​​数组中以循环的形式一个一个地取出​​dex​​​文件然后进行加载。​​dexElements​​​是在​​DexPathList​​对象构建的时候就已经赋值了(也就是在构造方法中)

三.热修复原理

学习了上面的知识后,就很容易明白热修复的原理了。

比如我的某一个类出bug了

Android虚拟机与类加载机制_class_18

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

Android虚拟机与类加载机制_class_19

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

Android虚拟机与类加载机制_jvm_20

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