一、Dalvik和ART虚拟机简介
在Java开发中一般使用的是HotSpot虚拟机,而在Andrpid应用程序则是运行在Dalvik/ART虚拟机中,每一个应用程序对应一个单独的Dalvik虚拟机示例。Dalvik也是Java虚拟机中的一种,只不过它执行的不是class文件而是dex文件。在Java中通常一个class对应一个字节码文件,而在Android中一个dex中可以包含多个class。
二、基于栈的虚拟机
在之前学习我们知道JVM是基于栈的虚拟机,即JVM运行时数据区中每一个线程都拥有独立的一个JAVA虚拟机栈,主要是用于存放方法的调用过程。JAVA虚拟机栈用于存放栈帧,每一个栈帧的入栈和出栈就是一个方法的执行过程。栈顶的栈帧表示当前正在执行的方法,在栈帧中包含了操作数栈和局部变量表,用来完成方法中的每一个操作和存放局部的变量。例如:int a = 1这条操作就包含了几个步骤:先将int 类型1 压入到操作数栈,再从操作数栈中出栈存放到局部变量表。而在Android的虚拟机中则是基于寄存器的虚拟机。没有了操作数栈。
三、基于寄存器的虚拟机
寄存器是CPU的组成部分,是有限存储容量的高速存储部件。它可以用来暂存指令、数据、和位址
如下:test方法
package com.haiheng.voiceandbook.utils;
public class Demo {
public static void test(){
int a =1;
int b =2;
int c = a+b;
}
}
对应下面的指令。
和JVM相似,每个线程都拥有自己的程序计数器和调用栈,方法的调用过程以栈帧为单位保存在调用栈上。只是在JVM中的指令需要通过在操作数栈和局部变量表中移动,而在DVM中,直接将数据指令存放在了虚拟的寄存器,并在上面计算结果。相对JVM来说指令变少了,数据移动次数也变少。
所以简单的来说就是在Android的虚拟机中没有了操作数栈的概念。当然具体的实现区别还有非常多。
四、Dalvik和ART的发展历程
- Dalvik虚拟机执行的是dex字节码,解释执行。从Android 2.2版本开始支撑JIT即时编译,在程序运行的时候讲经常执行的代码进行编译或者优化,保存记录。
- ART则是在Android4.4版本中引入的一个开发者选项,5.0版本开始默认使用的ART虚拟机。
- ART虚拟机执行的是本地的机器码,但是我们的APK中仍然是dex字节码,那么机器码那里来?
- 机器码则是通过在安装的过程编译成的机器码。
- ART引入了预先编译机制,即AOT。在安装的时候,ART使用设备自带的dex2oat工具来编译应用,dex字节码被编译成了机器码。这种情况下APK的安装就会变慢。
五、Android N的运作方式
而到了Android N以及之后的版本,则又采用了混合方式。AOT编译和解释执行、JIT。
(1)最初安装的时候不会进行AOT预编译,安装的时候又变快了,运行过程还是使用解释执行,经常执行的方法进行JIT,经过JIT编译的对经常使用的方法会记录到Profile配置文件中。
(2)在设备空闲充电的时候,编译守护进程会运行,根据JIT记录在Profile中的代码进行AOT编译成机器码,下次运行的时候直接运行机器码。
六、Android中的类加载器
之前在类加载机制中我们知道我们的类是通过类加载器ClassLoader来加载的,每一个类都有对应的类加载器。
6.1、Android中的类加载器
- BootClassLoader
用于加载Android Framework层中的class字节码文件 - PathClassLoader
主要是Android应用程序的类加载器,加载指定的dex、jar、zip、apk 中的 classes.dex。
6.2、双亲委托机制
某个类加载器在加载类的时候,首先委托上层父亲类加载器进行加载、如果父亲可以完成,则自己不需要重复加载。如果父亲不能完成,自己才去加载。
(1)避免重复加载、当父亲类加载器已经加载过某个类了,自己就不需要在加载。
(2)防止核心API串改,因为类加载器在加载某个类的时候,是通过类的全类名去找这个类,例如lang包下的String。这个类是由BootClassLoader加载的,如果我们在自己的项目写了一个一样包名的String类,这样PathClassLoader就不会重复去加载,因为BootClassLoader已经加载过。
6.3、ClassLoader源码分析
我们来分析一下Android中ClassLoader。主要是分析应用程序类加载器PathClassLoader。
tvDownLoad.setOnClickListener {
val classLoader = classLoader
Log.e(TAG, "onCreate: " + classLoader)
}
MainActivity: onCreate: dalvik.system.PathClassLoader
我们在当前Activity下打印的类加载器是PathClassLoader,这也说明了Activity类是通过应用程序类加载器加载的。我们来看下加载类的核心方法:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//1、检查类是否已经被加载,如果已经加载了,直接返回
Class> c = findLoadedClass(name);
if (c == null) {
//2、如果没有加载,委托父亲加载器BootClassLoader去加载
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
}
//3、父亲类加载器也没加载到,则自己去加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
(1)检查类是否已经被加载,如果已经加载了,直接返回
(2)如果没有加载,委托父亲加载器BootClassLoader去加载
(3)父亲类加载器也没加载到,则自己去加载。
接着调用PathClassLoader的父类BaseDexClassLoader中的findClass方法去寻找类加载。参数name就是全类名。
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
//调用pathList中的findClass去寻找类
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中的findClass去寻找类,pathList则是在BaseDexClassLoader的构造方法中创建的,
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
//调用DexPathList的构造方法,传入字节码路径dexPath,封装成一个DexPathList对象
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
主要作用是再通过DexPathList的构造方法,将我们的dex字节码路径dexPath解析封装成一个pathList 对象。而我们的dex文件可能有多个,所以在pathList 对象中包含了一个Element数组dexElements 。果是多个dex文件,dexPath则是通过冒号分割。源码中有体现。
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
ArrayList suppressedExceptions = new ArrayList();
// save dexPath for BaseDexClassLoader
//将我们传入的字节码路径dexPath,调用makeDexElements,解析成一个dexElements 数组,如果是多个dex文件,
//dexPath则是通过冒号分割。源码中有体现
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
..........
}
到这里我们知道我们的APK中可能会包含了多个dex文件,这些dex文件会解析成一个Element数组,每一个dex文件就是一个Element元素。而一个dex文件中会有多个class,因此类加载器寻找类加载的时候需要去遍历整个Element数组,再通过每个Element元素去查找符合要查找的类。最终会调用DexFile中的defineClassNative本地方法。一旦找到直接返回,不会继续往下找。
public Class> findClass(String name, List suppressed) {
for (Element element : dexElements) {
Class> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
....
return null;
}
public Class> findClass(String name, ClassLoader definingContext,
List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile)
6.4、字节码插装实现最简单版本的热修复
6.4.1、字节码插装分析
- 我们知道我们Android虚拟机加载的是dex文件,一个APK可能有多个dex,每一个dex 包含了多个class,在类加载器去寻找类加载的时候,首先将我们多个dex字节码封装成一个Element数组,每一个dex字节码对应一个Element元素,而一个dex中包含了多个class,因此在类加载器加载寻找类的时候,通过遍历Element数组,再根据Element元素调用本地方法去查找符合自己想要的类。一旦找到直接返回。
- 因此我们在字节码插装的时候,当我们的代码有BUG,我们可以重新 写一个class,编译成dex,放到我们的手机内存卡中,然后通过反射调用DexPathList中makeDexElements方法,将我们的dex解析成一个Element数组,然后继续通过反射获取DexPathList中的dexElements中的数组,将我们的数组和原来的数组合并,插到开头位置。然后通过反射设置dexElements的值为合并之后的数组。这样当我们的类加载器通过遍历dexElements数组一旦找到新类,就直接返回,就不会往后面寻找有BUG的类。
6.4.2、字节码插装简单实现
- 编写热修复代码。
package com.haiheng.voiceandbook.utils;
import android.app.Application;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ByteUtils {
private static final String TAG = "ByteUtils";
public static void init(Application application, File dexFile) {
//1、获取应用的ClassLoader对象 PathClassLoader
ClassLoader classLoader = application.getClassLoader();
//2、通过对象获取Class
Class clzz = classLoader.getClass();
//3、获取PathClassLoader父类的Class即 BaseDexClassLoader的Class
Class fatherClass = clzz.getSuperclass();
try {
//4、获取BaseDexClassLoader中的 DexPathList pathList成员(私有成员)
Field field = fatherClass.getDeclaredField("pathList");
//5、设置权限
if (!field.isAccessible()) {
field.setAccessible(true);
}
//6、拿到成员属性field之后,将field转化成pathList实例
//这样我们就通过反射拿到了BaseDexClassLoader中的pathList实例
//这里的classLoader是PathClassLoader,PathClassLoader继承了BaseDexClassLoader
//通过子类可以直接获取父类实例的成员变量
Object pathList = field.get(classLoader);
//7、调用pathList对象中makePathElements方法,将dex转换成Element[]数组
//(1)构建方法需要传递的参数optimizedDirectory、suppressedExceptions、files
File optimizedDirectory = application.getCacheDir();
ArrayList suppressedExceptions = new ArrayList();
ArrayList files = new ArrayList();
files.add(dexFile);
//(2)找到pathList对象中的 makePathElements方法
Method makePathElements = findMethod(pathList,"makePathElements", List.class, File.class,
List.class);
//(3)执行makePathElements方法,将dex转换成Element[]数组
Object patchElements [] = (Object[]) makePathElements.invoke(pathList, files, optimizedDirectory,
suppressedExceptions);
if(patchElements==null){
Log.e(TAG, "转换成patchElements失败");
}
else{
Log.e(TAG, "转换成patchElements成功"+patchElements.length);
}
//4、将我们的patchElements数组和原来的数组合并,插装到第一位
expandFieldArray(pathList,patchElements);
} catch (Exception e) {
Log.e(TAG, "init: "+e.getMessage() );
e.printStackTrace();
}
}
/**
* 将两个数组合并
* @param pathList
* @param patchElements
*/
private static void expandFieldArray(Object pathList, Object[] patchElements) {
//1、获取pathList对象原来的数组 dexElements
try {
//2、通过反射拿到Field成员,getDeclaredField获取私有的属性
Field field = pathList.getClass().getDeclaredField("dexElements");
if (!field.isAccessible()) {
field.setAccessible(true);
}
//3、将field成员转化成dexElements实例
Object[] dexElements = (Object[]) field.get(pathList);
//4、创建一个新的数组
Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
dexElements.length + patchElements.length);
//5、先拷贝新数组
System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
System.arraycopy(dexElements, 0, newElements, patchElements.length, dexElements.length);
//6、拷贝完毕之后设置到pathList实例的dexElements中
field.set(pathList,newElements);
Log.e(TAG, "字节码插装成功");
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "字节码插装失败"+e.getMessage());
}
}
/**
* 查找makePathElements方法
* @param instance
* @param name
* @param parameterTypes
* @return
* @throws NoSuchMethodException
*/
public static Method findMethod(Object instance, String name, Class>... parameterTypes)
throws NoSuchMethodException {
for (Class> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException e) {
// ignore and search next
}
}
throw new NoSuchMethodException("Method "
+ name
+ " with parameters "
+ Arrays.asList(parameterTypes)
+ " not found in " + instance.getClass());
}
}
- 在Application中初始化
override fun onCreate() {
super.onCreate()
ByteUtils.init(this,File("/sdcard/patch.dex"))
}
- 写一个有问题的类,调用的时候出现异常。
package com.haiheng.voiceandbook;
public class Test {
public void test(){
int i = 1/0;
}
}
java.lang.ArithmeticException: divide by zero
at com.haiheng.voiceandbook.Test.test(Test.java:6)
at com.haiheng.voiceandbook.MainActivity$onCreate$1.onClick(MainActivity.kt:22)
- 修改有问题的类,然后编译成dex,
package com.haiheng.voiceandbook;
import android.util.Log;
public class Test {
public void test(){
int i = 1/1;
Log.e("Test", "test: 没有热修复");
}
}
-
将dex上传到sdcard中,重新启动APP(不需要重新安装)
- 启动成功
2021-06-21 16:04:42.747 6058-6058/com.haiheng.voiceandbook E/Test: test: 修复成功
我们把dex文件放到内存卡,当Application启动的时候,就会去加载sdcard中的dex,而里面的 dex就包含了我们编写修复之后的类,通过反射机制,将我们的dex转换成element数组合并到原来的element数组,并且插装到最开始未知。这样在类加载器加载类的时候,加载到我们的新类,直接就返回,不会往后加载有BUG的类。