首页
社区
课程
招聘
[分享]基于虚拟机的类加载机制实现热修复
发表于: 2021-3-17 08:53 12552

[分享]基于虚拟机的类加载机制实现热修复

2021-3-17 08:53
12552

众所周知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期)

收藏
免费 5
支持
分享
最新回复 (1)
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
学习到了学习到了
2023-1-6 23:53
0
游客
登录 | 注册 方可回帖
返回
//