一.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了,这就完成了热修复。