0%

Java筑基-:(05)安卓虚拟机与类加载机制-1

一、JVM 与 Dalvik

JVM 与 Dalvik 实现不同:

  • JVM 是基于栈的虚拟机:每个运行时的线程都有一个独立的栈,每一次方法的调用都会往栈里面压入一个栈帧,最顶部的是当前栈帧,代表当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。

  • Dalvik 是基于寄存器的虚拟机:相当于将操作数栈和局部变量表合并成了虚拟寄存器

对于如下同一段代码:

1
2
3
4
5
public class Demo {
int a = 1;
int b = 2;
int c = a + b;
}

二者运行过程差异如下图所示:

二者差异

上图是JVM 运行过程

Dalvik的结构

上图是 Dalvik 的运行过程。

二、Dalvik 和 Art 区别

Android Dalvik 最开始是解释执行,执行的是 dex 字节码,需要经过虚拟机翻译,然后才能被机器执行。而从 2.2 开始,支持 JIT 即时编译(Just In Time),运行过程中将热点代码直接编译成机器码,后续就省略了翻译过程!因此效率更高,但也因此跨平台不行。

Android 5.0 最开始 ART 虚拟机执行的是本地机器码,但是 Dex 里面还是字节码。所以这个机器码从哪里来?原因是 安装做了优化,做了 AOT (Ahead Of Time)提前编译,做了 dex2oat 的优化,所以安装也慢(5.0 、6.0)。但是从 7.0 以后,又没那么慢了,变成混编了,混合了 AOT、解释 以及 JIT 这三种方式了。在运行过程中解释执行,对热点代码进行 JIT ,并且经过 JIT 编译的方法(这是临时的)记录到配置文件中。当设备空闲和充电的时候,编译守护进程会运行,根据 Profile 文件对常用代码进行 AOT 编译,下次就可以直接运行使用,而不用再从dex 文件中找了。

对比:

栈式 vs 寄存器式 对比
指令条数 栈式 > 寄存器式
移植性 栈式优于寄存器式
代码尺寸 栈式 < 寄存器式
指令优化 栈式更不易优化
解释器执行速度 栈式解释器速度稍慢
代码生成难度 栈式简单
简单实现中数据移动次数 栈式移动次数多

三、ClassLoder

  • BootClassLoader : 加载Android FrameWork 中的类,比如 String、Activity 等

  • PathClassLoader: 我们程序的 ClassLoader ,我们引入的第三方库、写的代码等

Android 都是自己写了这些 ClassLoader ,没有使用 Java 之前的那些,这个也能理解,原来Java 自己去加载类的时候,是加载 .class 文件;而 Android中是加载 .dex 文件了

ClassLoader 在 load 一个类的时候,都是需要 IO 操作的(从磁盘把文件读进来),然后按照一定格式解析(类似Json 一样),所以,这个 load 这个操作一定是个耗时操作。

注意,ClassLoader 做双亲委派机制,其中的 parent 不是指当前类的父类,这个 parent 只是 ClassLoader 中的一个成员变量而已。所以应该叫做父加载器更好点

比如,PathClassLoader 的父加载器是 BootClassLoder ,但是它的父类是 BaseDexClassLoader

3.1 为什么双亲委托机制

  • 避免重复。被父加载器加载过了,自己就不用加载了

  • 安全: 防止核心API 库被随意篡改。

    比如说你自己创建个 java.lang.String 类,如果没有双亲委派机制,那么你加载了你自己的 String 类,要是有崩溃,那么其他类基本上都会受影响,String 使用太广泛了。所以,双亲委派反正都是父ClassLoader 去加载,会导致都是同一个,不会有歧义。

3.2 PathClassLoader

里面可以有个 dexPath 参数,初看起来只能传入一个 dex 的路径,其实是可以传入多个的,多个路径以 冒号 (:) 分割即可。之后,为每个路径的dex 文件生成一个 Element 元素,最后形成 一个 dexElements 数组。

ClassLoader 中有个 DexPathList????存疑,自己去看下

四、热修复

修复了 Demo.java 这个类,之后打包成 dex 文件,然后想办法将其插入到 dexElements 数组的最前面,这样,找 Demo.java 这个类的时候,从 dexElements 数组从前往后找,先找到修复后的 Demo.java ,后面的有bug 的 Demo.java 就会不管了。这样就能实现热修复了。

可以在 Application 的第一个方法中插入新的 dex ,要保证需要修复的类没有被加载过。因为被加载过就有这个类的缓存了,就不会从 dex 文件中去找了。

4.1 将补丁 dex 插入到 dexElements 数组最前面

这里肯定需要用到反射,具体步骤如下:

  1. 获取到当前应用的 PathClassLoader

  2. 反射获取到它的属性对象 pathList

  3. 反射修改 pathList 的 dexElements ,这里又分为 3 个步骤

    1. 把补丁包 patch.dex 转化为 Element[] 数组

    2. 获得 pathList 的 dexElements 属性 dexElements

    3. 将上述新生成的 Element 数组,与老的 dexElements 合并生成新的数组 newPatchElements ,之后将这个 newPatchElements 赋值给上述的 pathList 的成员变量

怎么把单个的类打包成 dex ?在 build-tools 目录下,使用里面的 dx 来打包,命令如下:

dx –dex –output=output.dex /packagename/A.class

4.2 我们为什么不能修改系统的类,只能修改自己的类呢?

这是因为双亲委派机制,系统的类(比如 String 类)都是通过 BootClassLoader 去加载的,而我们的类是 PathClassLoader 加载的。并且 PathClassLoader 的父加载器是 BootClassLoader !

五、答疑环节

前面讲到Android N 的时候,会有一些代码变成 机器码了,所以,这时候我们用上述的方法,是不能完成热修复的。那咋办?Tinker 自己创建了一个 ClassLoader ,这样系统就不会使用系统给的 ClassLoader ,缓存的机器码自然也就不存在了,还是会去找dex去执行。

作业:自己实现一个 热修复的代码Demo。,补丁包直接放在 sd 卡里面即可。

这里需要注意在 Android 10.0 的时候,对存储权限有格外要求,你可以放在私有目录

谢谢你的鼓励