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

一、Dalvik和ART虚拟机简介

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


二、基于栈的虚拟机

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

三、基于寄存器的虚拟机

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


如下图:test方法



对应下面的指令。



和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中的类加载器

  • 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: 没有热修复");
    }
}

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


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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章