一 前言介紹
正好最近又看到熱更新,對以前Android 熱修復核心原理:ClassLoader類加載機制做了點補充,
從16年開始開始,熱修復技術開始在安卓界流行,它以classloader類加載機制為核心,可以不發布新版本就修復線上 bug ,讓線上版本有能力去進行全量或者增量更新,
常見的思路有兩種:
-
類加載方案,即 dex 插樁,該方案以騰訊系為主,包括微信的 Tinker、餓了么的 Amigo;
-
底層替換,即修改替換 ArtMethod,方案以阿里系的 AndFix 等為主;
本文主要介紹第一種方案,
1.1 ART 和 Dalvik
-
Dex :全稱為Dalvik Executable Format,由很多 .class 檔案處理壓縮后的產物,最終可以在 Android 運行時環境執行,它適合于記憶體和處理器速度有限的系統,
-
Dalvik:Google設計的Android平臺的Java虛擬機,支持轉換為.dex格式的Java程式運行,DVM默認使用CMS垃圾回收器,
-
ART:Android Runtime,于Android 4.4 引入,在 Android 5.0 及更高版本作為默認的 Android 運行時,ART做出的具體改進可看安卓官方檔案介紹:運行時:Android Runtime (ART) 和 Dalvik,ART 和 Dalvik 都是運行 Dex 位元組碼的兼容運行時,因此 ART 向下兼容Dalvik 開發的應用,
-
AOT:ART在應用安裝的時候預編譯位元組碼到機器語言,這一機制叫Ahead-Of-Time(AOT)預編譯,執行該操作后,應用程式安裝會變慢,但是執行將更有效率,啟動更快,
1.2 dexopt與dexaot
-
dexopt:Dalvik虛擬機加載dex檔案時,會對 dex 檔案進行驗證和優化,得到odex(Optimized dex) 檔案,odex檔案只是對dex檔案使用了一些優化操作碼,
-
dex2oat:dex或者odex檔案經過 AOT 預編譯,即得到OAT(實際上是ELF檔案)可執行檔案(機器碼),(相比做過odex優化,未做過優化的dex轉換成OAT要花費更長的時間)

1.3 ART 和 Dalvik 對比
-
在Dalvik下,應用運行需要解釋執行,常用熱點代碼通過即時編譯器(JIT)將位元組碼轉換為機器碼,運行效率低,而在ART 環境中,應用在安裝時,位元組碼預編譯(AOT)成機器碼,安裝慢了,但運行效率會提高,
-
ART占用空間比Dalvik大(位元組碼變為機器碼), “空間換時間",
-
預編譯也可以明顯改善電池續航,因為應用程式每次運行時不用重復編譯了,從而減少了 CPU 的使用頻率,降低了能耗,
二 ClassLoader
2.1 Android運行流程
Android程式編譯的時候,會將.java檔案編譯時.class檔案,然后將.class檔案打包為.dex檔案,Android程式運行時,Android的Dalvik/ART虛擬機就會加載.dex檔案從中獲得.class檔案到記憶體中來使用,
2.2 類加載工具ClassLoader
任何 Java 程式都是由一個或多個 class 檔案組成,在程式運行時,需要通過 Java 的類加載機制將 class 檔案加載到 JVM 中才可以使用,Java程式啟動時不會一次性加載所有類,而是先把保證運行的基礎類加載到jvm,其它類要用時再加載,這樣的好處是節省了記憶體的開銷,用時再加載這也是java動態性的一種體現,
這些類的加載就是通過ClassLoader來的,每個 Class 物件的內部都有一個 classLoader 欄位來標識自己是由哪個 ClassLoader 加載的,安卓的ClassLoader小改了java的ClassLoader,
class Class<T> {
...
private transient ClassLoader classLoader;
...
}
常見的Android類加載器有如下四種:
- BootClassLoader :加載Android Framework層中的class位元組碼檔案(類似java的BootstrapClassLoader)
- PathClassLoader :只能加載已經安裝到Android系統中的Apk的 class 位元組碼檔案,是Android默認使用的類加載器;(類似java的 AppClassLoader )
- DexClassLoader :可以加載加載制定目錄的dex/jar/apk/zip檔案檔案(類似java中的 Custom ClassLoader ),比 PathClassLoader 更靈活,是實作熱修復的重點;
- BaseDexClassLoader : PathClassLoader 和 DexClassLoader 的父類
Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加載");
//輸出:
Activity.class 由:java.lang.BootClassLoader@b1202a1 加載
MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加載
它們之間的關系如下:

public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
super(dexPath, null, librarySearchPath, parent);
}
}
PathClassLoader 與 DexClassLoader 在建構式中都呼叫了父類的建構式,兩者唯一的區別在于:DexClassLoader多傳了一個optimizedDirectory引數,并且會將其創建為File物件傳給super,而PathClassLoader則直接給到null,因此兩者都可以加載指定的dex,以及jar、zip、apk中的classes.dex
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());
其實optimizedDirectory引數就是dexopt的產出目錄(odex),DexClassLoader不僅僅可以加載 dex檔案,還可以加載jar、apk、zip檔案中的dex,而jar、apk、zip其實就是一些壓縮格式,要拿到壓縮包里面的dex檔案就需要解壓,所以,DexClassLoader在呼叫父類建構式時會指定一個解壓的目錄,那PathClassLoader創建時,這個目錄為null,就意味著不進行dexopt?并不是,optimizedDirectory為null時的默認路徑為:/data/dalvik-cache,
在API 26原始碼中,將DexClassLoader的optimizedDirectory標記為了 deprecated 棄用,實作也變得和PathClassLoader一摸一樣了:
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
2.3 雙親委托機制
可以看到創建ClassLoader需要接收一個ClassLoader parent引數,這個parent的目的就在于實作類加載的雙親委托,即:某個類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞回,如果父類加載器可以完成類加載任務,就成功回傳;只有父類加載器無法完成此加載任務時,才自己去加載,
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 檢查class是否有被加載
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果parent不為null,則呼叫parent的loadClass進行加載
c = parent.loadClass(name, false);
} else {
//parent為null,則呼叫BootClassLoader進行加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果都找不到就自己查找
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
值得注意的是:c = findBootstrapClassOrNull(name);按照方法名理解,應該是當parent為null時候,也能夠加載BootClassLoader加載的類,但是實際上,Android當中的實作為:(Java不同)
private Class findBootstrapClassOrNull(String name)
{
return null;
}
2.4 類加載器的三個機制(約束)

雙親委托機制實際上是一種自上而下帶快取的加載,這種機制也決定它的一些特性:
委托:將類加載交由父類加載器加載,父不行再自己加載,
可見性:子類加載器可見所有的父類加載器加載的類,父類加載器不可見子類加載器加載的類,
單一性:一個類僅加載一次,子類加載器不會再次加載父類加載器加載過的類,
2.5 findClass
可以看到在所有父ClassLoader無法加載Class時,則會呼叫自己的findClass方法,findClass在ClassLoader中的定義為:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
其實任何ClassLoader子類,都可以重寫loadClass與findClass,一般如果你不想使用雙親委托,則重寫loadClass修改其實作,而重寫findClass則表示在雙親委托下,父ClassLoader都找不到Class的情況下,定義自己如何去查找一個Class,而我們的PathClassLoader會自己負責加載MainActivity這樣的程式中自己撰寫的類,利用雙親委托父ClassLoader加載Framework中的Activity,說明PathClassLoader并沒有重寫loadClass,因此我們可以來看看PathClassLoader中的 findClass 是如何實作的,
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String
librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath,
optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//查找指定的class
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中查找class,繼續查看DexPathList:
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//.........
// splitDexPath 實作為回傳 List<File>.add(dexPath)
// makeDexElements 會去 List<File>.add(dexPath) 中使用DexFile加載dex檔案回傳 Element陣列
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
//.........
}
public Class findClass(String name, List<Throwable> suppressed) {
//從element中獲得代表Dex的 DexFile
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//查找class
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
可以看到, BaseDexClassLoader 的 findClass() 方法實際上是通過 DexPathList 的 findClass() 方法來獲取class的,而這個 DexPathList 物件恰好在之前的 BaseDexClassLoader 建構式中就已經被創建好了,里面決議了dex檔案的路徑,并將決議的dex檔案都存在this.dexElements里面,DexPathList 類通過建構式呼叫了 makeDexElements() 得到 Element 集
makeDexElements():
//決議dex檔案
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<I
// 1.創建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍歷所有dex檔案(也可能是jar、apk或zip檔案)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex檔案
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip檔案(這部分在不同的Android版本中,處理方式有細微差別)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.將dex檔案或壓縮檔案包裝成Element物件,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.將Element集合轉成Element陣列回傳
return elements.toArray(new Element[elements.size()]);
}
可以看到,DexPathList 的建構式是將一個個的程式檔案(可能是dex、apk、jar、zip)封裝成一個個 Element 物件,最后添加到Element集合中,Android的類加載器(不管是PathClassLoader,還是DexClassLoader,它們最后在加載檔案時,都只認dex檔案,而loadDexFile()是加載dex檔案的核心方法,他可以可以從jar、apk、zip中提取出dex,
回頭看一下PathClassLoader中的 findClass 方法:
Class c = pathList.findClass(name, suppressedExceptions);
于是看到DexPathList的findClass()方法,如下:
public Class findClass(String name, List<Throwable> suppressed) {
// 遍歷從dexPath查詢到的dex和資源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果當前的Element是dex檔案元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加載類
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
對 Element 陣列進行遍歷,一旦找到類名與name相同的類時,就直接回傳這個 class ,找不到則回傳null,
通過如上分析,我們發現整個類加載流程就是:
- 類加載器
BaseDexClassLoader先將dex檔案決議放到pathList到dexElements里面 - 加載類的時候從
dexElements里面去遍歷,看哪個dex里面有這個類就去加載,生成class物件
三 熱修復
接剛剛個類加載流程,熱修復原理就是將補丁 dex 檔案放到 dexElements 陣列靠前位置,這樣在加載 class 時,優先找到補丁包中的 dex 檔案,加載到 class 之后就不再尋找,從而原來的 apk 檔案中同名的類就不會再使用,從而達到修復的目的:
在PathClassLoader中的Element陣列為:[patch.dex , classes.dex , classes2.dex],如果存在Key.class位于patch.dex與classes2.dex中都存在一份,當進行類查找時,回圈獲得dexElements中的DexFile,查找到了Key.class則立即回傳,不會再管后續的element中的DexFile是否能加載到Key.class了,因此一種熱修復實作可以將出現Bug的class單獨的制作一份fix.dex檔案(補丁包),然后在程式啟動時,從服務器下載fix.dex保存到某個路徑,再通過fix.dex的檔案路徑,用其創建Element物件,然后將這個Element物件插入到我們程式的類加載器PathClassLoader的pathList中的dexElements陣列頭部,這樣在加載出現Bug的class時會優先加載fix.dex中的修復類,從而解決Bug,
熱修復的方式不止這一種,并且如果要完整實作此種熱修復可能還需要注意一些其他的問題(如:反射兼容),另外插件的形式常見的有apk和dex檔案,
dex打包工具(d8)
更新的dex檔案如何生成呢?
Android SDK提供了dex打包工具d8,在在Android 構建工具 28.0.1 及更高版本中可以找到:

對正常的java檔案,直接javac成class檔案后,可直接用d8編譯成dex檔案:
./d8 XXX.class
如果你想更新的檔案是apk格式的,可直接在Android Studio中對更新的Module/Lib直接打包成apk,
代碼實作
dex替換:
//在Application中進行替換
public class MApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//dex作為插件進行加載
dexPlugin();
}
...
/**
* dex作為插件加載
*/
private void dexPlugin(){
//插件包檔案
File file = new File("/sdcard/hotfix.dex");
if (!file.exists()) {
Log.i("MApplication", "插件hotfix不存在");
return;
}
try {
//獲取到 BaseDexClassLoader 的 pathList欄位
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破壞封裝,設定為可以呼叫
pathListField.setAccessible(true);
//拿到當前ClassLoader的pathList物件
Object pathListObj = pathListField.get(getClassLoader());
//獲取當前ClassLoader的pathList物件的位元組碼檔案(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements欄位
// private final Element[] dexElements;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破壞封裝,設定為可以呼叫
dexElementsField.setAccessible(true);
//使用插件創建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList物件
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList物件的 dexElements變數
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//拿到當前的pathList物件的 dexElements變數
Object dexElementsObj=dexElementsField.get(pathListObj);
int oldLength = Array.getLength(dexElementsObj);
int newLength = Array.getLength(newDexElementsObj);
//創建一個dexElements物件
Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);
//先添加新的dex添加到dexElement
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));
}
//再添加之前的dex添加到dexElement
for (int i = 0; i < oldLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));
}
//將組建出來的物件設定給 當前ClassLoader的pathList物件
dexElementsField.set(pathListObj, concatDexElementsObject);
} catch (Exception e) {
e.printStackTrace();
}
}
apk:
// apk作為插件加載
private void apkPlugin() {
//插件包檔案
File file = new File("/sdcard/hotfix.apk");
if (!file.exists()) {
Log.i("MApplication", "插件hotfix不存在");
return;
}
try {
//獲取到 BaseDexClassLoader 的 pathList欄位
// private final DexPathList pathList;
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
//破壞封裝,設定為可以呼叫
pathListField.setAccessible(true);
//拿到當前ClassLoader的pathList物件
Object pathListObj = pathListField.get(getClassLoader());
//獲取當前ClassLoader的pathList物件的位元組碼檔案(DexPathList )
Class<?> dexPathListClass = pathListObj.getClass();
//拿到DexPathList 的 dexElements欄位
// private final Element[] dexElements;
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
//破壞封裝,設定為可以呼叫
dexElementsField.setAccessible(true);
//使用插件創建 ClassLoader
DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
//拿到插件的DexClassLoader 的 pathList物件
Object newPathListObj = pathListField.get(pathClassLoader);
//拿到插件的pathList物件的 dexElements變數
Object newDexElementsObj = dexElementsField.get(newPathListObj);
//將插件的 dexElements物件設定給 當前ClassLoader的pathList物件
dexElementsField.set(pathListObj, newDexElementsObj);
} catch (Exception e) {
e.printStackTrace();
}
}
一些擴展
-
選擇apk檔案的方式一般就不對dexElements陣列就行插前值操作了,而是直接替換整個dexElements陣列,但是現實中,熱更新可能只是更新某幾個類或者資源檔案,如果使用apk全量替換的方式,就很重,那么增量替換,即使用dex檔案的方式,就是很好的方式;
-
更新的檔案一般放服務器上需要客戶端下載插前值;
-
so庫在Android代碼中是通過呼叫System.loadLibrary函式實作的,動態注冊的native方法呼叫一次JNI_OnLoad方法都會重新完成一次映射, 所以我們是否只要先加載原來的so庫,,然后再加載補丁so庫,就能完成Java層native方法到native層patch后的新方法映射, 這樣就完成動態注冊native方法的patch實時修復,
-
、資源檔案的更新方式:加載apk,反射呼叫AssetManager的addAssetPath方法,
參考文章:
Android Runtime (ART) 和 Dalvik
Android 熱修復核心原理, ClassLoader類加載
【小家Java】從原理層面理解Java中的類加載器
一看你就懂,超詳細java中的ClassLoader詳解
Android熱修復實作及原理
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/302092.html
標籤:其他
上一篇:從需求分解到實作實戰系列之 - 1圖片串列拖動選中功能
下一篇:Java的概述
