Android 虚拟机与类加载机制
文章目录
- Android 虚拟机与类加载机制
- 一. Android 虚拟机版本
- 1.1 概述
- 1.2 基于栈的虚拟机
- 1.2.1 字节码指令
- 1.2.2 执行过程
- 1.3 基于寄存器的虚拟机
- 1.3.1 执行过程
- 1.3.2 和基于栈的虚拟机的区别
- 1.4 ART 和Dalvik的区别
- 1.4.1 ARTx虚拟机执行的本地机器码是从哪里来的
- 1.4.2 安装时进行预编译带来的问题
- 1.4.2 Android N的运作方式
- 二. ClassLoader
- 2.1 常见的类加载器
- 2.2 类加载流程
- 2.3 为什么不直接在dexElements中查找
- 三. 热修复原理
- 3.1 热修复原理分析
- 3.2 热修复代码实现
一. Android 虚拟机版本
1.1 概述
Android 应用程序运行在Dalvik/ART 虚拟机上,并且每个应用程序对应一个单独的Dalvik虚拟机实例。Dalvik虚拟机实则也是一个Java虚拟机,只不过Java虚拟机执行的是Class文件,而Dalvik虚拟机执行的是dex文件。
Dalvik虚拟机和Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基于寄存器的,而后者的指令集是基于栈的。
1.2 基于栈的虚拟机
对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。栈中记录了方法的调用历史,每有一次方法的调用,栈中便会多出来一个栈帧。最顶部的栈帧乘坐当前栈帧,其代表着当前正在执行的方法。基于栈的虚拟机通过操作数栈进行所有的操作。
1.2.1 字节码指令
1.2.2 执行过程
- 首先将int类型常量压入操作数栈
- 然后将操作数栈顶int类型的值存入到局部变量表0的位置
- 将int类型的常量压入操作数栈
- 将操作数栈定的int类型的值存入局部变量表中1的位置
- 将局部变量表0的变量的值压入操作数栈
- 将局部便令表1的变量的值压入操作数栈
- 执行相加操作
- 将相加的结果存入局部变量表2的位置
- 然后返回。
1.3 基于寄存器的虚拟机
基于寄存器的虚拟机中没有操作数栈,但有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和方法调用栈,方法调用的活动记录以栈帧为单位保存在调用栈上。
1.3.1 执行过程
- 将操作数存放到虚拟寄存器V0的位置
- 将操作数存放到虚拟寄存器V1的位置
- 执行相加操作,将结果存放到虚拟寄存器V2的位置上
- 返回
1.3.2 和基于栈的虚拟机的区别
很明显,我们可以发现,基于寄存器的虚拟机对同样的操作执行的执行更少了,数据移动的次数也更少了。
1.4 ART 和Dalvik的区别
Dalvik执行的是dex字节码,解释执行。从Android 2.2版本开始,支持JIT即时编译(Just in Time),在程序运行过程中进行选择热点代码(经常执行的代码)进行编译或优化。
而ART(Android Runtime)是在Android 4.4中引入的一个开发者选项,也是Android 5.0以及更高版本的默认Android运行时。ART虚拟机执行的是本地机器码。Android的运行时从Dalvik虚拟机替换成ART虚拟机并不需要开发者将自己的应用直接编译成目标机器吗,APK仍然是一个包含dex字节码文件的apk。
1.4.1 ARTx虚拟机执行的本地机器码是从哪里来的
Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件。而ART下将应用的dex字节码翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候。ART引用了预先编译(Ahead Of Time)机制,在安装时,ART使用设备自带的dex2oat工具来编译应用,dex中的字节码将被编译成本地机器码。
1.4.2 安装时进行预编译带来的问题
安装变的很慢,因为要将dex字节码编译成本地机器码。
1.4.2 Android N的运作方式
ART使用预先(AOT)编译,并且从Android N混合使用AOT编译,解释和JIT。
- 最初安装应用的时候不进行任何AOT编译(安装又变快了),运行过程中解释执行,对经常执行的代码进行JIT,经过JIT编译的方法将会记录到Profile配置文件中。
- 当设备闲置和充电时,编译守护进行会运行,根绝Profile文件对常用代码进行AOT编译,待下次运行时直接使用。
二. ClassLoader
ClassLoader:类加载器,专门负责加载类的对象。ClassLoader类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的 “类文件”。
每个Class对象都包含一个对定义它的ClassLoader的引用。
数组类的Class对象不是由类加载器创建的,而是由Java运行时根据需要自动创建。数组类的类加载器由Class.getClassLoader()返回,该加载器与元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组类没有类加载器。
应用程序需要实现ClassLoader的子类,以扩展Java虚拟机动态加载类的方式。
2.1 常见的类加载器
ClassLoader 是一个抽象类,有两个子类,分别是BootClassLoader,BaseDexClassLoader,其中BootClassLoader负责加载Android Framework层的class文件,
在系统启动的时候便会调用BootClassLoader。
PathClassLoader 是我们应用程序的类加载器,只能加载系统中已经安装过的apk,DexClassLoader 主要用于加载未安装的jar /apk/dex
2.2 类加载流程
此处以PathClassLoader 为例来说明类的加载过程
继承自BaseDexClassLoader
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实例的时候会根据传入的dex文件的地址去找dex文件,然后把文件放入到一个dexElements的数组中
调用loadClass方法的时候会先调用findLoadedClass在缓存中查找,如果有直接返回,如果没有则从父类加载器中查找,一层一层往上找,如果父类中有则返回,如果没有,那么调用自己的findClass在dexElements中查找,如果有就返回,没有报异常。
2.3 为什么不直接在dexElements中查找
- 如果缓存中有则直接返回,速度快
- 安全,可以防止用户修改核心类,通过上面的加载流程可以发现,首先从缓存中找,找不到从父类加载器中找,因为核心类是在系统启动时候就加载了的,所以一般可以在缓存中找到,再只就是优先从父类加载器中找,这样就可以避免优先加载用户自定义的同限定名的类了。
三. 热修复原理
3.1 热修复原理分析
从上面的类加载机制中可以看到,用户自己定义的类一般是在dexElements中找的,我们看一下怎么查找的:
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
for 循环匹配,那么我们在dexElements的数组最前面加入我们要修复的dex文件就可以达到修复的问题,如下图:
正常的文件数组,要加载的为dex1:
首先遍历dex0 ,然后dex1,找到之后返回
假如说dex1这时候有bug了,我们需要修复,那么我们可以把dex1加到数组最前面,这样在遍历的时候首先找到的就是我们修复好的dex,这样就达到了热修复的目的。
如下图:
就这样,我们把新的dex1插入到数组的最前面,在查找的时候便会首先返回的是新的dex1。热修复的思路就是这样的。
3.2 热修复代码实现
有空再完善