十一、Android虚拟机和类加载机制

一、Dalvik和ART虚拟机简介

在Java开发中一般使用的是HotSpot虚拟机,而在Andrpid应用程序则是运行在Dalvik/ART虚拟机中,每一个应用程序对应一个单独的Dalvik虚拟机示例。Dalvik也是Java虚拟机中的一种,只不过它执行的不是class文件而是dex文件。在Java中通常一个class对应一个字节码文件,而在Android中一个dex中可以包含多个class。


image.png

二、基于栈的虚拟机

在之前学习我们知道JVM是基于栈的虚拟机,即JVM运行时数据区中每一个线程都拥有独立的一个JAVA虚拟机栈,主要是用于存放方法的调用过程。JAVA虚拟机栈用于存放栈帧,每一个栈帧的入栈和出栈就是一个方法的执行过程。栈顶的栈帧表示当前正在执行的方法,在栈帧中包含了操作数栈和局部变量表,用来完成方法中的每一个操作和存放局部的变量。例如:int a = 1这条操作就包含了几个步骤:先将int 类型1 压入到操作数栈,再从操作数栈中出栈存放到局部变量表。而在Android的虚拟机中则是基于寄存器的虚拟机。没有了操作数栈。

三、基于寄存器的虚拟机

寄存器是CPU的组成部分,是有限存储容量的高速存储部件。它可以用来暂存指令、数据、和位址


image.png

如下:test方法

package com.haiheng.voiceandbook.utils;

public class Demo {
    public static void test(){
        int a =1;
        int b =2;
        int c = a+b;
    }
}

对应下面的指令。


image.png

和JVM相似,每个线程都拥有自己的程序计数器和调用栈,方法的调用过程以栈帧为单位保存在调用栈上。只是在JVM中的指令需要通过在操作数栈和局部变量表中移动,而在DVM中,直接将数据指令存放在了虚拟的寄存器,并在上面计算结果。相对JVM来说指令变少了,数据移动次数也变少。
所以简单的来说就是在Android的虚拟机中没有了操作数栈的概念。当然具体的实现区别还有非常多。

四、Dalvik和ART的发展历程

  • Dalvik虚拟机执行的是dex字节码,解释执行。从Android 2.2版本开始支撑JIT即时编译,在程序运行的时候讲经常执行的代码进行编译或者优化,保存记录。
  • ART则是在Android4.4版本中引入的一个开发者选项,5.0版本开始默认使用的ART虚拟机。
  • ART虚拟机执行的是本地的机器码,但是我们的APK中仍然是dex字节码,那么机器码那里来?
  • 机器码则是通过在安装的过程编译成的机器码。
  • ART引入了预先编译机制,即AOT。在安装的时候,ART使用设备自带的dex2oat工具来编译应用,dex字节码被编译成了机器码。这种情况下APK的安装就会变慢。

五、Android N的运作方式

而到了Android N以及之后的版本,则又采用了混合方式。AOT编译和解释执行、JIT。
(1)最初安装的时候不会进行AOT预编译,安装的时候又变快了,运行过程还是使用解释执行,经常执行的方法进行JIT,经过JIT编译的对经常使用的方法会记录到Profile配置文件中。
(2)在设备空闲充电的时候,编译守护进程会运行,根据JIT记录在Profile中的代码进行AOT编译成机器码,下次运行的时候直接运行机器码。

六、Android中的类加载器

之前在类加载机制中我们知道我们的类是通过类加载器ClassLoader来加载的,每一个类都有对应的类加载器。

6.1、Android中的类加载器

image.png
  • BootClassLoader
    用于加载Android Framework层中的class字节码文件
  • PathClassLoader
    主要是Android应用程序的类加载器,加载指定的dex、jar、zip、apk 中的 classes.dex。

6.2、双亲委托机制

某个类加载器在加载类的时候,首先委托上层父亲类加载器进行加载、如果父亲可以完成,则自己不需要重复加载。如果父亲不能完成,自己才去加载。
(1)避免重复加载、当父亲类加载器已经加载过某个类了,自己就不需要在加载。
(2)防止核心API串改,因为类加载器在加载某个类的时候,是通过类的全类名去找这个类,例如lang包下的String。这个类是由BootClassLoader加载的,如果我们在自己的项目写了一个一样包名的String类,这样PathClassLoader就不会重复去加载,因为BootClassLoader已经加载过。

6.3、ClassLoader源码分析

我们来分析一下Android中ClassLoader。主要是分析应用程序类加载器PathClassLoader。

  tvDownLoad.setOnClickListener {
            val classLoader = classLoader
            Log.e(TAG, "onCreate: " + classLoader)

        }
MainActivity: onCreate: dalvik.system.PathClassLoader

我们在当前Activity下打印的类加载器是PathClassLoader,这也说明了Activity类是通过应用程序类加载器加载的。我们来看下加载类的核心方法:

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
          //1、检查类是否已经被加载,如果已经加载了,直接返回
            Class<?> c = findLoadedClass(name);
            if (c == null) {
              //2、如果没有加载,委托父亲加载器BootClassLoader去加载
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
              //3、父亲类加载器也没加载到,则自己去加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

(1)检查类是否已经被加载,如果已经加载了,直接返回
(2)如果没有加载,委托父亲加载器BootClassLoader去加载
(3)父亲类加载器也没加载到,则自己去加载。
接着调用PathClassLoader的父类BaseDexClassLoader中的findClass方法去寻找类加载。参数name就是全类名。

   @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
      //调用pathList中的findClass去寻找类
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

调用pathList中的findClass去寻找类,pathList则是在BaseDexClassLoader的构造方法中创建的,

  public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
//调用DexPathList的构造方法,传入字节码路径dexPath,封装成一个DexPathList对象
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }

主要作用是再通过DexPathList的构造方法,将我们的dex字节码路径dexPath解析封装成一个pathList 对象。而我们的dex文件可能有多个,所以在pathList 对象中包含了一个Element数组dexElements 。果是多个dex文件,dexPath则是通过冒号分割。源码中有体现。

 public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {

     
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
      //将我们传入的字节码路径dexPath,调用makeDexElements,解析成一个dexElements 数组,如果是多个dex文件,
  //dexPath则是通过冒号分割。源码中有体现
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);

      ..........
    }

到这里我们知道我们的APK中可能会包含了多个dex文件,这些dex文件会解析成一个Element数组,每一个dex文件就是一个Element元素。而一个dex文件中会有多个class,因此类加载器寻找类加载的时候需要去遍历整个Element数组,再通过每个Element元素去查找符合要查找的类。最终会调用DexFile中的defineClassNative本地方法。一旦找到直接返回,不会继续往下找。

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

     ....
        return null;
    }
      public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }
    private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                                  DexFile dexFile)

6.4、字节码插装实现最简单版本的热修复

6.4.1、字节码插装分析

  • 我们知道我们Android虚拟机加载的是dex文件,一个APK可能有多个dex,每一个dex 包含了多个class,在类加载器去寻找类加载的时候,首先将我们多个dex字节码封装成一个Element数组,每一个dex字节码对应一个Element元素,而一个dex中包含了多个class,因此在类加载器加载寻找类的时候,通过遍历Element数组,再根据Element元素调用本地方法去查找符合自己想要的类。一旦找到直接返回。
  • 因此我们在字节码插装的时候,当我们的代码有BUG,我们可以重新 写一个class,编译成dex,放到我们的手机内存卡中,然后通过反射调用DexPathList中makeDexElements方法,将我们的dex解析成一个Element数组,然后继续通过反射获取DexPathList中的dexElements中的数组,将我们的数组和原来的数组合并,插到开头位置。然后通过反射设置dexElements的值为合并之后的数组。这样当我们的类加载器通过遍历dexElements数组一旦找到新类,就直接返回,就不会往后面寻找有BUG的类。

6.4.2、字节码插装简单实现

  • 编写热修复代码。

package com.haiheng.voiceandbook.utils;

import android.app.Application;
import android.util.Log;

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.Arrays;
import java.util.List;

public class ByteUtils {

    private static final String TAG = "ByteUtils";

    public static void init(Application application, File dexFile) {
        //1、获取应用的ClassLoader对象 PathClassLoader
        ClassLoader classLoader = application.getClassLoader();
        //2、通过对象获取Class
        Class clzz = classLoader.getClass();
        //3、获取PathClassLoader父类的Class即 BaseDexClassLoader的Class
        Class fatherClass = clzz.getSuperclass();

        try {
            //4、获取BaseDexClassLoader中的 DexPathList pathList成员(私有成员)
            Field field =  fatherClass.getDeclaredField("pathList");
            //5、设置权限
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            //6、拿到成员属性field之后,将field转化成pathList实例
            //这样我们就通过反射拿到了BaseDexClassLoader中的pathList实例
            //这里的classLoader是PathClassLoader,PathClassLoader继承了BaseDexClassLoader
            //通过子类可以直接获取父类实例的成员变量
            Object pathList = field.get(classLoader);


            //7、调用pathList对象中makePathElements方法,将dex转换成Element[]数组
            
            //(1)构建方法需要传递的参数optimizedDirectory、suppressedExceptions、files
            File optimizedDirectory = application.getCacheDir();
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ArrayList<File> files = new ArrayList<File>();
            files.add(dexFile);
            
            //(2)找到pathList对象中的  makePathElements方法
            Method makePathElements = findMethod(pathList,"makePathElements", List.class, File.class,
                    List.class);
            //(3)执行makePathElements方法,将dex转换成Element[]数组
            Object patchElements []  = (Object[]) makePathElements.invoke(pathList, files, optimizedDirectory,
                    suppressedExceptions);

            if(patchElements==null){
                Log.e(TAG, "转换成patchElements失败");
            }
            else{
                Log.e(TAG, "转换成patchElements成功"+patchElements.length);
            }
            //4、将我们的patchElements数组和原来的数组合并,插装到第一位
            expandFieldArray(pathList,patchElements);


        } catch (Exception e) {
            Log.e(TAG, "init: "+e.getMessage() );
            e.printStackTrace();
        }


    }

    /**
     * 将两个数组合并
     * @param pathList
     * @param patchElements
     */
    private static void expandFieldArray(Object pathList, Object[] patchElements) {
        //1、获取pathList对象原来的数组 dexElements
        try {
            //2、通过反射拿到Field成员,getDeclaredField获取私有的属性
            Field field  = pathList.getClass().getDeclaredField("dexElements");
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            //3、将field成员转化成dexElements实例
            Object[] dexElements = (Object[]) field.get(pathList);

            //4、创建一个新的数组
            Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
                    dexElements.length + patchElements.length);

            //5、先拷贝新数组
            System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
            System.arraycopy(dexElements, 0, newElements, patchElements.length, dexElements.length);

            //6、拷贝完毕之后设置到pathList实例的dexElements中
            field.set(pathList,newElements);
            Log.e(TAG, "字节码插装成功");


        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "字节码插装失败"+e.getMessage());
        }


    }

    /**
     * 查找makePathElements方法
     * @param instance
     * @param name
     * @param parameterTypes
     * @return
     * @throws NoSuchMethodException
     */
    public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
            throws NoSuchMethodException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Method method = clazz.getDeclaredMethod(name, parameterTypes);

                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }

                return method;
            } catch (NoSuchMethodException e) {
                // ignore and search next
            }
        }
        throw new NoSuchMethodException("Method "
                + name
                + " with parameters "
                + Arrays.asList(parameterTypes)
                + " not found in " + instance.getClass());
    }

}

  • 在Application中初始化
override fun onCreate() {
        super.onCreate()
        ByteUtils.init(this,File("/sdcard/patch.dex"))
  
    }
  • 写一个有问题的类,调用的时候出现异常。
package com.haiheng.voiceandbook;

public class Test {

    public void test(){
        int i = 1/0;
    }
}

    java.lang.ArithmeticException: divide by zero
        at com.haiheng.voiceandbook.Test.test(Test.java:6)
        at com.haiheng.voiceandbook.MainActivity$onCreate$1.onClick(MainActivity.kt:22)
  • 修改有问题的类,然后编译成dex,
package com.haiheng.voiceandbook;

import android.util.Log;

public class Test {

    public void test(){
        int i = 1/1;
        Log.e("Test", "test: 没有热修复");
    }
}

image.png
  • 将dex上传到sdcard中,重新启动APP(不需要重新安装)


    image.png
  • 启动成功
2021-06-21 16:04:42.747 6058-6058/com.haiheng.voiceandbook E/Test: test: 修复成功

我们把dex文件放到内存卡,当Application启动的时候,就会去加载sdcard中的dex,而里面的 dex就包含了我们编写修复之后的类,通过反射机制,将我们的dex转换成element数组合并到原来的element数组,并且插装到最开始未知。这样在类加载器加载类的时候,加载到我们的新类,直接就返回,不会往后加载有BUG的类。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容