前言
在上一篇文章中【JVM入門食用指南-03】JVM垃圾回收器以及性能調優[1],對垃圾回收器以及JVM性能調優進行了敘述,并對JVM中CMS垃圾回收器進行了重點闡述,通過之前在JVM中的鋪墊,本文我們回到Android中,本文我們主要對 Android虛擬機和類加載機制進行敘述,加深對 Android 的理解,
即將學會
- ART 和 Dalvik
- Android 類加載流程
- 熱修復實作原理
ART 與 Dalvik
JVM 與 DVM
JVM 是我們的 Java 虛擬機,而我們的Android應用運行在 Dalvik/ART 虛擬機上的,每一個應用對應一個 Dalvik 虛擬機,雖然 Dalvik 虛擬機也算 Java 虛擬機,不過執行的不是.class檔案,而是dex檔案,

這里對上述名詞進行一些敘述
虛擬機分類
基于堆疊的虛擬機 每一個運行的執行緒,都有一個獨立的堆疊,堆疊中記錄了方法呼叫的歷史,每一個方法的呼叫,都對應一個堆疊幀,并且將堆疊幀壓入堆疊中,最頂部的堆疊幀為當前堆疊幀,既 當前執行的方法,基于堆疊幀 與 運算元堆疊進行所有操作,
暫存器 CPU的組成部分,暫存器是有限存盤容量的高速存盤容器(計算機中各級存盤速度 暫存器-> 一級快取->二級快取 -> 記憶體 -> 硬碟),可以用來管理指令、資料的地址,
基于暫存器的虛擬機 (模擬暫存器的操作流程)
基于暫存器的虛擬機中沒有運算元堆疊,不過有很多虛擬暫存器,類似運算元堆疊,這些暫存器也存放在運行時堆疊中,本質上就是一個陣列,與JVM相似,在Dalvik 虛擬機中每個執行緒都有自己的PC 和 呼叫堆疊,方法呼叫的活動記錄以幀為單位保存在呼叫堆疊上,
不同虛擬機流程
我們對比一下 基于堆疊的流程 和 基于暫存器的流程

JVM流程分析參考之前文章分析,通常需要經過以下操作
- 如將堆疊頂某型別值存入區域變數某處
- 將區域變數中某處某型別常量壓入運算元堆疊
- 執行相關指令

DVM 中,直接依據指令,將某型別值存入虛擬暫存器中,相關的操作也依據指令在虛擬暫存器中通過CPU處理,將處理結果回傳虛擬暫存器(省掉了基于堆疊的流程中區域變數與運算元堆疊中資料的流動)

ART 與 Dalvik 區別

ART 和 Dalvik 都是基于暫存器的虛擬機
Dalvik 執行 dex位元組碼,解釋執行,在Android2.2 開始,支持即時編譯,既選擇程式中經常執行的代碼(又稱熱點代碼),進行編譯和優化 減少了JVM中解釋的流程,
ART虛擬機 (Android4.4) 直接跑機器碼,不進行位元組碼解釋,Android的運行時從Dalvik 無影響過渡ART,因為并不用開發者將應用直接編譯成目標機器碼,APk依舊是一個包含dex位元組碼的檔案,
為什么APK依舊是包含dex位元組碼的檔案,而ART和 Dalvik的執行方式不一樣?
這里ART直接跑位元組碼,而Dalvik進行解釋執行,并在2.2引入JIT,但是都是dex檔案,
因為APK檔案需要在Android系統中運行,需要進行安裝,這個時候,經過虛擬機,會對dex進行優化,Dalvik中,會將dex位元組碼進行優化生成odex檔案,而ART 會將 dex位元組碼翻譯為本地機器碼(引入了預先編譯機制 Ahead of Time),安裝時,ART使用設備自帶的dex2oat工具對應用進行編譯,dex中的位元組碼被編譯成本地機器碼,這也是為什么我們安裝的時候比較慢,
不過后面從Android O 開始,安裝速度變快了,因為這個時候采用混合編譯了,JIT、AOT、和位元組碼解釋, 當應用首次安裝時,不進行AOT編譯,運行程序中采用解釋執行,并將程式中經常要執行的代碼進行即時編譯(JIT),并將JIT編譯后的方法記錄到Profile組態檔,當設備閑置時,利用編譯守護行程,依據Profile檔案對常用檔案進行AOT編譯(生成.art檔案),以便下次運行時直接使用,如果下次程式運行,將檢測該檔案并決議出機器碼中檔案中的類資訊,加載到ClassLoader中,
ClassLoader 及 類加載流程
ClassLoader
每一個Java程式都是有一個或多個class檔案組成,在程式運行時,需要將class檔案加載到JVM中才可以使用,而加載class檔案的是Java的類加載機制,ClassLoader加載class檔案提供程式運行時使用,每個Class物件內部都有一個classLoader欄位來標識由何種classLoader加載,
ClassLoader 類 以及 類結構


- ClassLoader
- BootClassLoader 用于加載Android Framework 層 Class檔案(像String.Class)
- PathClassLoader 繼承自ClassLoader 用于Android應用程式加載類
類加載流程
PathClassLoader
下面我們分析一下PathClassLoader類來分析類加載流程
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
*/
// Android-removed: Remove references to getClassLoadingLock
// Remove perf counters.
//
// <p> Unless overridden, this method synchronizes on the result of
// {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
// during the entire class loading process.
//
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//找快取,已經加載過的就不會被加載了
//class檔案需要轉換為class物件 耗時操作
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
//沒有快取進if
if (c == null) {
try {
//如果該類下的parent物件實體不為空,利用該parent物件加載相關類
//該物件指向 BootClassLoader 如下圖所示
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//parent 為空,回傳空
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//如果 parent(BootstrapClass)中加載不到該類
//則 呼叫自身findClass去加載類(PathClassLoader)
c = findClass(name);
}
}
//有快取直接回傳
return c;
}
/**
* Returns a class loaded by the bootstrap class loader;
* or return null if not found.
*/
private Class<?> findBootstrapClassOrNull(String name)
{
return null;
}
我們可以通過上述代碼分析 和 PathClassLoader類結構圖得到該類的loadClass流程

那么為什么我們ClassLoader中要進行這樣的設計呢 ?
首先,這個parent的目的在于實作類加載的雙親委托,首先加載任務委托給父類加載器,依次遞回,如果父類加載器可以完成類加載任務,則成功回傳,只有父類加載器無法完成此加載任務時,才自己去完成加載, 而實作這種機制,可以達到以下效果
- 避免重復加載,當parent對應的加載器已經加載了該類的時候,就沒有必有當前classloader再加載一次,直接在parent中的快取找,
- 安全考慮,防止核心API被篡改,像自己定義一個String.java 這個時候classloader會先加載framework中的String.java(加載只要全限定名一樣,可以偽造構建和系統類一樣的全限定名)
上面我們分析了它的設計思想,回歸加載程序中,這里用到了findClass(name) 方法
findClass(name) 又做了什么?
我們可以通過查看其自身和父類尋找findClass方法,通過查看
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//通過pathList 去找 相關 類,而pathList 是 DexPathList 是 final 的
Class c = pathList.findClass(name, suppressedExceptions);
//知道了 回傳 Class相關實體物件,沒找到拋例外 關鍵在上一行代碼
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 宣告 是不可變的 我們去查看其賦值
private final DexPathList pathList;
//pathList 又是由什么賦值 可以看到相關使用在構造方法中
// 通過查看其構造方法,可知我們需要傳入 dexPath 去 實體化 DexPathList
// 因此PathClassLoader才可以加載到程式的所有類
/**
* @hide
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
//我們跳去 DexPathList 看看
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
// 我們看一下DexPathList
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
//常規 引數判空
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
// 這里使用到了 dexpath
// splitDexPath 將dexpath的多地址分離 , PathClassLoader 可以處理多個dex
// makeDexElements 依據dexpath生成多個dexElement
// 下面我們去看一下splitDexPath 和 makeDexElements
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
//splitDexPath 方法
private static List<File> splitDexPath(String path) {
return splitPaths(path, false);
}
/**
* Splits the given path strings into file elements using the path
* separator, combining the results and filtering out elements
* that don't exist, aren't readable, or aren't either a regular
* file or a directory (as specified). Either string may be empty
* or {@code null}, in which case it is ignored. If both strings
* are empty or {@code null}, or all elements get pruned out, then
* this returns a zero-element list.
*/
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
List<File> result = new ArrayList<>();
if (searchPath != null) {
for (String path : searchPath.split(File.pathSeparator)) {
if (directoriesOnly) {
try {
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode)) {
continue;
}
} catch (ErrnoException ignored) {
continue;
}
}
result.add(new File(path));
}
}
return result;
}
//makePathElements 方法
private static final String zipSeparator = "!/";
private static NativeLibraryElement[] makePathElements(List<File> files) {
// 一個dex 生成一個 elements 物件
NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
int elementsPos = 0;
for (File file : files) {
String path = file.getPath();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
File zip = new File(split[0]);
String dir = split[1];
elements[elementsPos++] = new NativeLibraryElement(zip, dir);
} else if (file.isDirectory()) {
// We support directories for looking up native libraries.
elements[elementsPos++] = new NativeLibraryElement(file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
這里,我們了解到了pathList這個物件為了findClass做了什么準備,現在,我們回到BaseClassLoader 中的 findClass 方法中,繼續看pathList這一行代碼,pathList這個實體我們已經知曉了,現在看它呼叫的findClass方法(和BaseClassLoader中不一樣)
//在這里,我們可以看到 該方法使用到了剛才 pathList 中 dexElements陣列
// 通過遍歷 每一個 Element 遍歷所有的dex
// DexFile dex = element.dexFile; 拿到對應的 DexFile
// dex.loadClassBinaryName(name, definingContext, suppressed);
// 再通過每個dex 的 loadClassBinaryName 去將類找出
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
大致的類加載流程就如上所示
熱修復原理
那么,通過類加載流程,我們這個時候可以在這之上進行一下熱修復

當我們程式有Bug時,我們對相關類進行修改,并將修改后的類打包成補丁dex,當客戶端下載時,只要讓補丁包加載在類加載之前,就可以提前加載到虛擬機中,而有錯誤的類,因為類加載機制問題,不會重復加載,從而不會加載到錯誤類,進而實作修復,
熱修復原理大致代碼流程
//相關正常修復類打補丁包
dx --dex --output=output.dex xxx.jar
dx --dex --output=output.dex 類全限定名
在Application類中 重寫 attachBaseContext 并在super后 呼叫 熱修復
并傳入Application,補丁dex檔案至熱修復類
//熱修復類中
//1、獲得classloader(PathClassLoader)
ClassLoader classLoader = application.getClassLoader();
//2\. 不同的Android 版本 ,熱修復有所差異,需要對其適配 (以6.0為例)
//3\. 遍歷該類及超類
Class<?> clazz = classLoader.getClass(); clazz != null; clazz = clazz.getSuperclass()
//4.通過反射 獲取 pathList 屬性成員
Field field = clazz.getDeclaredField("pathList");
//5.獲取到pathList物件
Object dexPathList = pathListField.get(classLoader);
///將自己的補丁包轉換為Element陣列 參考PathClassLoader
// 得到補丁包的Element陣列
// 反射拿到 makePathElements 方法
Class<?> clazz = dexPathList.getClass(); clazz != null; clazz = clazz.getSuperclass()
Method method = clazz.getDeclaredMethod("makePathElements", parameterTypes);
//反射執行方法 回傳 補丁包對應的 Element陣列
Object[] patchElements = (Object[]) method.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions)
//將原本的 dexElements 與 makePathElements生成的陣列合并,并利用反射塞回 dexElements
//拿到 classloader中的dexelements 陣列
Class<?> clazz = dexPathList.getClass(); clazz != null; clazz = clazz.getSuperclass()
Field dexElementsField = clazz.getDeclaredField("dexElements");
//old Element[]
Object[] dexElements = (Object[]) dexElementsField.get(dexPathList);
//合并后的陣列
Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
dexElements.length + patchElements.length);
// 先拷貝新陣列
System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
System.arraycopy(dexElements, 0, newElements, patchElements.length, dexElements.length);
//修改 classLoader中 pathList的 dexelements
dexElementsField.set(dexPathList, newElements);
上述只是依據類加載原理進而對熱修復原理進行闡述的大致流程,實作熱修復還需要考慮到一下兼容問題、混淆等內容,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/296627.html
標籤:其他
上一篇:Android中順轉跳/逆回傳到多個Activity+堆疊(附含GitHub源代碼下載)
下一篇:算下來,Android開發也已發行多時,移動 App 已經趨近飽和,那么 Android 開發還會有那么吃香嗎?
