admin管理员组

文章数量:1530085

关于虚拟机

虚拟机是什么?

众所周知Java程序是运行在虚拟机(JVM)上的,而安卓之前的官方语言正是Java,所以在安卓中也会存在虚拟机的概念。虚拟机存在的意义是什么呢?其实虚拟机相当于一个“翻译官”的角色,Java语言无法直接与系统进行交互,而虚拟机便起到了一个翻译的作用。我们经常提到Java是一个跨平台、平台无关的编程语言,也正是因为不管是Linux还是Windows操作系统,只要有虚拟机做翻译我们的程序便可正常运行,同样的也不管语言差别,只要虚拟机可以翻译便可以与系统进行正常的交互。

虚拟机的工作流程

以Java虚拟机(JVM)为例,它的工作流程大致如下:

1、因为在JVM中执行的是class文件,因此首先要借助javac将java文件编译成class文件

2、通过类加载器将class加载到运行时数据区(也就是我们常说的加载到内存中)

3、通过执行引擎与操作系统提供的接口交互

Android的虚拟机

在安卓中,提供了Dalvik和Art两种虚拟机,在Android 4.4发布之前一直用的是Dalvik虚拟机,后面引用并在5.0之后默认使用Art虚拟机。Art是相对于Dalvik来说性能和效率会有一定的提升,但是在首次安装的时候却会更加耗时,这是因为二者采用的编译机制不同。在安卓虚拟机中,运行的是dex字节码,从Android 2.2之前,Dalvik是通过解释执行的方式运行字节码,之后为了提高效率引进了JIT即时编译机制,支持在程序运行的过程中对那些经常执行的代码(热点代码)进行编译或优化。而Art则与Dalvik不同,Art则是在应用安装的过程中将字节码编译成机器码,也就是AOT预先编译机制。

Android虚拟机与Java虚拟机的区别

以Davlik为例,与JVM主要存在三个区别:

1、运行的文件不同,在JVM中运行的是经过javac编译之后的class文件,而在Dalvik中运行的是dex字节码,需要借助dx工具将class转换成dex文件

2、应用体积更小,借助dx工具将class转成dex文件的过程中,会对代码进行一些优化,比如一些重复的方法等只会保留一份,所以体积会变小

3、运行速度更快,在JVM中方法的调用主要是基于栈实现的,所以需要大量的入栈出栈,而Dalvik则是基于寄存器实现的,因此速度会更快,性能会有明显的提升

关于类加载机制

前面提到虚拟机会将class加载到内存中,那么是怎么加载的呢?这就用到了今天的主角ClassLoader,首先我们先通过ClassLoader的继承关系图了解几个关键的类。

1、ClassLoader:是一个抽象类,所有类加载器的基类,无需过多介绍

2、BootClassLoader:主要负责Framework层class的加载器

3、PathClassLoader:主要负责加载安卓应用层的class

4、DexClassLoader:是安卓系统额外提供给我们的一个动态类加载器

5、DexPathList:主要负责解析dex并以一个Element数组存储dex信息

完整的类加载机制

1、初始化类加载器,同时会初始化一个DexPathList对象pathList,并解析dex文件,以一个Element数组的形式存储dex信息

2、我们会调用类加载器的loadClass,然后调用findClass方法

3、调用类加载器的findClass会调用该pathList的findClass方法

4、pathList中findClass遍历Element数组,逐个解析加载

5、从Element中取出DexFile,并调用其loadClassBinaryName完成类的加载

类加载机制的核心源码分析

1、loacClass实现

首先我们看一下ClassLoader中的loadClass方法是如何实现的,如上图所示类的加载是基于双亲委托机制实现的,大致可以分为三步:

1、检查class是否被加载过

2、判断parent是否为空,决定是调用BootClassLoader还是parent的loadClass方法

3、如果前两步还没加载成功,则自己进行查找

为什么要使用双亲委托机制呢?主要是考虑到了两方面的原因:

1、避免重复加载

2、防止核心的api被恶意篡改

2、findClass实现

调用pathList的findClass,如果结果返回null则抛出异常

3、DexPathList的findClass实现

我们可以看到在上面的代码中,是通过一个for循环遍历Element数组,取出存储的DexFile对象,然后再调用DexFile的loadClassBinaryName,再往后的代码咱们暂时没有继续的必要了,为什么呢?

我们的目的就是基于虚拟机的类加载机制,实现一个简单的热修复。看到这里相信大家都已经有思路了,既然是遍历数组,那么我们就可以通过在数组的第一个位置插入一个新的dex数据实现热修复。

热修复的简单实现

准备工作

1、待修复应用

我们自己创建一个安卓应用,自己定义一个TestUtil类并实现一个test方法,抛出一个异常,关键代码如下:

为了让效果更加明显,我们用一个try-catch捕获异常并用Toast显示

代码写完了,我们运行一下看看效果。

接下来,我们开始热修复的工作。

2、用于修复异常的dex文件

前面提到了虚拟机上运行的是dex文件,因此为了实现热修复我们需要一个用于修复的dex文件。

首先,我们修改一下test方法,注释掉抛出异常的代码,然后build一下,通过javac将java文件编译成class文件

然后我们利用dx工具将class文件转成dex文件

这样就生成了我们需要的dex文件

热修复的实现

准备工作做好了之后,我们拥有了待修复的应用以及所需的dex文件,接下来的工作就是如何热修复?整个热修复的过程,主要分为以下几步:

1、首先获取类加载器

2、获取到类加载器的Class

3、反射获取DexPathList对象pathList

4、反射获取Element数组dexElements

5、获取补丁数组

6、合并两个数组

7、替换dexElements为合并之后的数组

8、调用安装补丁的方法

核心代码如下:

自定义Application调用安装补丁的方法,代码如下:

需要注意几点问题:

1、在这里只是针对7.1版本,没有考虑适配问题

2、项目中只用了一个dex补丁作为简单的热修复演示

3、千万不要忘记了权限

存在的安全性问题

热修复的核心就是动态加载dex,那么问题来了?dex如何获取,如何存储,如何保障其不会被篡改或者破坏?所以我们要做热修复的话需要保证dex文件的安全,可以通过dex加密等手段来保障dex文件不会被篡改和破坏。除此之外,dex文件存放的目录要尽可能的隐蔽,不建议像本次示例程序一样将dex文件放在固定外部存储目录中。

本文标签: 虚拟机加载机制android