二十一、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的類。

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