基于虚拟机的类加载机制实现热修复

  • A+
所属分类:安全闲碎
基于虚拟机的类加载机制实现热修复

本文为看雪论坛优秀文章

看雪论坛作者ID:镜中人24



关于虚拟机




1. 虚拟机是什么?


众所周知Java程序是运行在虚拟机(JVM)上的,而安卓之前的官方语言正是Java,所以在安卓中也会存在虚拟机的概念。虚拟机存在的意义是什么呢?其实虚拟机相当于一个“翻译官”的角色,Java语言无法直接与系统进行交互,而虚拟机便起到了一个翻译的作用。

我们经常提到Java是一个跨平台、平台无关的编程语言,也正是因为不管是Linux还是Windows操作系统,只要有虚拟机做翻译我们的程序便可正常运行,同样的也不管语言差别,只要虚拟机可以翻译便可以与系统进行正常的交互。

2. 虚拟机的工作流程


基于虚拟机的类加载机制实现热修复

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

(1) 因为在JVM中执行的是class文件,因此首先要借助javac将java文件编译成class文件。
(2) 通过类加载器将class加载到运行时数据区。(也就是我们常说的加载到内存中)

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


3. Android的虚拟机


在安卓中,提供了Dalvik和Art两种虚拟机,在Android 4.4发布之前一直用的是Dalvik虚拟机,后面引用并在5.0之后默认使用Art虚拟机。Art是相对于Dalvik来说性能和效率会有一定的提升,但是在首次安装的时候却会更加耗时,这是因为二者采用的编译机制不同。

在安卓虚拟机中,运行的是dex字节码,从Android 2.2之前,Dalvik是通过解释执行的方式运行字节码,之后为了提高效率引进了JIT即时编译机制,支持在程序运行的过程中对那些经常执行的代码(热点代码)进行编译或优化。而Art则与Dalvik不同,Art则是在应用安装的过程中将字节码编译成机器码,也就是AOT预先编译机制。

4. 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. 完整的类加载机制


基于虚拟机的类加载机制实现热修复


a. 初始化类加载器,同时会初始化一个DexPathList对象pathList,并解析dex文件,以一个Element数组的形式存储dex信息。
b. 我们会调用类加载器的loadClass,然后调用findClass方法。
c. 调用类加载器的findClass会调用该pathList的findClass方法。
d. pathList中findClass遍历Element数组,逐个解析加载。

e. 从Element中取出DexFile,并调用其loadClassBinaryName完成类的加载。


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


(1) loacClass实现


基于虚拟机的类加载机制实现热修复

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

(a) 检查class是否被加载过。


(b) 判断parent是否为空,决定是调用BootClassLoader还是parent的loadClass方法。


(c) 如果前两步还没加载成功,则自己进行查找。


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

* 避免重复加载。
* 防止核心的api被恶意篡改。
(2) findClass实现

基于虚拟机的类加载机制实现热修复

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

(3) DexPathList的findClass实现


基于虚拟机的类加载机制实现热修复

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

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



热修复的简单实现




1. 准备工作


(1) 待修复应用

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


基于虚拟机的类加载机制实现热修复

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

基于虚拟机的类加载机制实现热修复

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

基于虚拟机的类加载机制实现热修复

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


(2) 用于修复异常的dex文件

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


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


基于虚拟机的类加载机制实现热修复

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

基于虚拟机的类加载机制实现热修复

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

2. 热修复的实现


准备工作做好了之后,我们拥有了待修复的应用以及所需的dex文件,接下来的工作就是如何热修复?整个热修复的过程,主要分为以下几步:
a. 首先获取类加载器;
b. 获取到类加载器的Class;
c. 反射获取DexPathList对象pathList;
d. 反射获取Element数组dexElements;
e. 获取补丁数组;
f. 合并两个数组;
g. 替换dexElements为合并之后的数组;
h. 调用安装补丁的方法。
核心代码如下:
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;    }}

自定义Application调用安装补丁的方法,代码如下:
protected void attachBaseContext(Context base) {        super.attachBaseContext(base);        String dexPath = "/storage/emulated/0/Android/data/classes.dex";        File file = new File(dexPath);        if(file!=null && file.exists()) {            HotFix.installDex(this, dexPath);        }    }

需要注意几点问题:
1、在这里只是针对7.1版本,没有考虑适配问题。
2、项目中只用了一个dex补丁作为简单的热修复演示。
3、千万不要忘记了权限。


存在的安全性问题




热修复的核心就是动态加载dex,那么问题来了?dex如何获取,如何存储,如何保障其不会被篡改或者破坏?所以我们要做热修复的话需要保证dex文件的安全,可以通过dex加密等手段来保障dex文件不会被篡改和破坏。

除此之外,dex文件存放的目录要尽可能的隐蔽,不建议像本次示例程序一样将dex文件放在固定外部存储目录中。


 


基于虚拟机的类加载机制实现热修复

- End -



基于虚拟机的类加载机制实现热修复


看雪ID:镜中人24

https://bbs.pediy.com/user-home-919715.htm

  *本文由看雪论坛 镜中人24 原创,转载请注明来自看雪社区。






# 往期推荐






基于虚拟机的类加载机制实现热修复
公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]



基于虚拟机的类加载机制实现热修复

球分享

基于虚拟机的类加载机制实现热修复

球点赞

基于虚拟机的类加载机制实现热修复

球在看



基于虚拟机的类加载机制实现热修复

点击“阅读原文”,了解更多!

本文始发于微信公众号(看雪学院):基于虚拟机的类加载机制实现热修复

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: