众所周知Java程序是运行在虚拟机(JVM)上的,而安卓之前的官方语言正是Java,所以在安卓中也会存在虚拟机的概念。虚拟机存在的意义是什么呢?其实虚拟机相当于一个“翻译官”的角色,Java语言无法直接与系统进行交互,而虚拟机便起到了一个翻译的作用。我们经常提到Java是一个跨平台、平台无关的编程语言,也正是因为不管是Linux还是Windows操作系统,只要有虚拟机做翻译我们的程序便可正常运行,同样的也不管语言差别,只要虚拟机可以翻译便可以与系统进行正常的交互。
以Java虚拟机(JVM)为例,它的工作流程大致如下:
1、因为在JVM中执行的是class文件,因此首先要借助javac将java文件编译成class文件
2、通过类加载器将class加载到运行时数据区(也就是我们常说的加载到内存中)
3、通过执行引擎与操作系统提供的接口交互
在安卓中,提供了Dalvik和Art两种虚拟机,在Android 4.4发布之前一直用的是Dalvik虚拟机,后面引用并在5.0之后默认使用Art虚拟机。Art是相对于Dalvik来说性能和效率会有一定的提升,但是在首次安装的时候却会更加耗时,这是因为二者采用的编译机制不同。在安卓虚拟机中,运行的是dex字节码,从Android 2.2之前,Dalvik是通过解释执行的方式运行字节码,之后为了提高效率引进了JIT即时编译机制,支持在程序运行的过程中对那些经常执行的代码(热点代码)进行编译或优化。而Art则与Dalvik不同,Art则是在应用安装的过程中将字节码编译成机器码,也就是AOT预先编译机制。
以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完成类的加载
首先我们看一下ClassLoader中的loadClass方法是如何实现的,如上图所示类的加载是基于双亲委托机制实现的,大致可以分为三步:
1、检查class是否被加载过
2、判断parent是否为空,决定是调用BootClassLoader还是parent的loadClass方法
3、如果前两步还没加载成功,则自己进行查找
为什么要使用双亲委托机制呢?主要是考虑到了两方面的原因:
1、避免重复加载
2、防止核心的api被恶意篡改
调用pathList的findClass,如果结果返回null则抛出异常
我们可以看到在上面的代码中,是通过一个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文件放在固定外部存储目录中。
package com.android.hotfix;
import
android.app.Application;
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.
List
;
public
class
HotFix {
public static void installDex(Application myApp, String path){
/
/
获取类加载器
ClassLoader loader
=
myApp.getClassLoader();
/
/
获取到类加载器的Class
Class
cls
=
loader.getClass();
/
/
获取pathList对象
Field plField
=
null;
Object
pathList
=
null;
try
{
plField
=
cls
.getSuperclass().getDeclaredField(
"pathList"
);
plField.setAccessible(true);
pathList
=
plField.get(loader);
} catch (Exception e) {
e.printStackTrace();
}
/
/
获取Element数组
Object
[] dexElements
=
null;
Field dexField
=
null;
if
(pathList!
=
null){
try
{
dexField
=
pathList.getClass().getDeclaredField(
"dexElements"
);
dexField.setAccessible(true);
dexElements
=
(
Object
[]) dexField.get(pathList);
} catch (Exception e) {
e.printStackTrace();
}
}
/
/
获取补丁数组
Object
[] patchElements
=
makePatch(myApp,pathList,path);
if
(patchElements!
=
null && dexElements !
=
null){
/
/
合并两个数组
Object
[] newElements
=
(
Object
[]) Array.newInstance(dexElements[
0
].getClass(),dexElements.length
+
patchElements.length);
System.arraycopy(patchElements,
0
,newElements,
0
,patchElements.length);
System.arraycopy(dexElements,
0
,newElements,patchElements.length,dexElements.length);
/
/
替换合并后的数组
try
{
dexField.
set
(pathList,newElements);
} catch (Exception e) {
e.printStackTrace();
}
try
{
Object
[] testField
=
(
Object
[]) dexField.get(pathList);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
private static
Object
[] makePatch(Application myApp,
Object
pathList, String path) {
Object
[] rst
=
null;
try
{
/
/
获取makeElements方法
/
/
Method makeMethod
=
pathList.getClass().getDeclaredMethod(
"makePathElements"
, java.util.
List
.
class
,java.io.
File
.
class
,java.util.
List
.
class
);
Method makeMethod
=
pathList.getClass().getDeclaredMethod(
"makeElements"
, java.util.
List
.
class
,java.io.
File
.
class
,java.util.
List
.
class
,boolean.
class
,ClassLoader.
class
);
/
/
参数准备
List
<
File
> dexFiles
=
new ArrayList<>();
File
dexFile
=
new
File
(path);
dexFiles.add(dexFile);
File
optimizedDirectory
=
myApp.getCacheDir();
List
<IOException> suppressedExceptions
=
new ArrayList<>();
/
/
调用方法
if
(makeMethod!
=
null) {
makeMethod.setAccessible(true);
/
/
return
(
Object
[]) makeMethod.invoke(pathList,dexFiles,optimizedDirectory,suppressedExceptions);
return
(
Object
[]) makeMethod.invoke(pathList,dexFiles,optimizedDirectory,suppressedExceptions,false,myApp.getClassLoader());
}
} catch (Exception e) {
e.printStackTrace();
}
return
null;
}
}
package com.android.hotfix;
import
android.app.Application;
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.
List
;
public
class
HotFix {
public static void installDex(Application myApp, String path){
/
/
获取类加载器
ClassLoader loader
=
myApp.getClassLoader();
/
/
获取到类加载器的Class
Class
cls
=
loader.getClass();
/
/
获取pathList对象
Field plField
=
null;
Object
pathList
=
null;
try
{
plField
=
cls
.getSuperclass().getDeclaredField(
"pathList"
);
plField.setAccessible(true);
pathList
=
plField.get(loader);
} catch (Exception e) {
e.printStackTrace();
}
/
/
获取Element数组
Object
[] dexElements
=
null;
Field dexField
=
null;
if
(pathList!
=
null){
try
{
dexField
=
pathList.getClass().getDeclaredField(
"dexElements"
);
dexField.setAccessible(true);
dexElements
=
(
Object
[]) dexField.get(pathList);
} catch (Exception e) {
e.printStackTrace();
}
}
/
/
获取补丁数组
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)