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