Android平臺上虛擬機(jī)運(yùn)行的是Dex字節(jié)碼,一種對class文件優(yōu)化的產(chǎn)物,傳統(tǒng)Class文件是一個(gè)Java源碼文件會生成一個(gè).class文件,而Android是把所有Class文件進(jìn)行合并,優(yōu)化,然后生成一個(gè)最終的class.dex,目的是把不同class文件重復(fù)的東西只需保留一份,如果我們的Android應(yīng)用不進(jìn)行分dex處理,最后一個(gè)應(yīng)用的apk只會有一個(gè)dex文件。
Android平臺的ClassLoader

我們主要用到了兩種類加載器 PathClassLoader 和 DexClassLoader 。
他們的區(qū)別是:
PathClassLoader:是 Android 應(yīng)用中默認(rèn)的類加載器,只能加載已經(jīng)安裝到 Android 系統(tǒng)中的apk文件(/data/app目錄下,解壓為 dex 后優(yōu)化為 odex)。
DexClassLoader:可以加載路徑下的 dex/jar/apk/zip 文件,比 PathClassLoader 更靈活,是實(shí)現(xiàn)熱修復(fù)的關(guān)鍵。
首先說下雙親委托模式
雙親委托模式的特點(diǎn)
類加載器查找Class所采用的是雙親委托模式,所謂雙親委托模式就是首先判斷該Class是否已經(jīng)加載,如果沒有則不是自身去查找而是委托給父加載器進(jìn)行查找,這樣依次的進(jìn)行遞歸,直到委托到最頂層的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了該Class,就會直接返回,如果沒找到,則繼續(xù)依次向下查找,如果還沒找到則最后會交由自身去查找。
這樣講可能會有些抽象,來看下面的圖。

再說dex文件的加載和類的查找過程
Java層通過我們會通過創(chuàng)建一個(gè)DexClassLoader來加載我們的dex,下面就以此為切入點(diǎn)進(jìn)行
創(chuàng)建ClassLoader
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());
查看DexClassLoader的構(gòu)造方法。
public class DexClassLoader extends BaseDexClassLoader {
// dexPath:是加載apk/dex/jar的路徑
// optimizedDirectory:是優(yōu)化dex后得到的.odex文件的輸出路徑
// libraryPath:是加載的時(shí)候需要用到的so庫
// parent:給DexClassLoader指定父加載器
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看到它調(diào)用的是父類的構(gòu)造函數(shù),所以直接來看BaseDexClassLoader的構(gòu)造函數(shù)。
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
創(chuàng)建了一個(gè)DexPathList實(shí)例,下面來看看DexPathList的構(gòu)造函數(shù)。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
}
//它調(diào)用的是makeDexElements方法來創(chuàng)建一個(gè)Element數(shù)組來存放Element對象,
//每個(gè)Element對象包含一個(gè)DexFile對象。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
// 如果是一個(gè)dex文件
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
// 如果是一個(gè)apk或者jar或者zip文件
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
// 1、調(diào)用loadDexFile加載dex文件,得到一個(gè)DexFile對象
// loadDexFile通過c++層native方法去加載dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
// 2、把DexFile對象封裝到Element對象中,然后將Element對象加入Element數(shù)組
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
dex文件的加載流程:我們會使用DexClassLoader去加載dex文件,DexClassLoader會將這個(gè)任務(wù)委派給DexPathList中的makeDexElements方法,在makeDexElements中調(diào)用了native層的 c++方法去真正的加載dex文件,然后返回DexFile的對象,通過這個(gè)對象構(gòu)建一個(gè)Element的對象,然后將這個(gè)Element添加到dexElements的數(shù)組中。
類的加載過程
當(dāng)類加載器收到加載類或資源的請求時(shí),通常都是先委托給父類加載器加載,也就是說只有當(dāng)父類加載器找不到指定類或資源時(shí),自身才會執(zhí)行實(shí)際的類加載過程,源碼如下
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 首先從已經(jīng)加載的類中查找
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 如果沒有加載過,先調(diào)用父加載器的 loadClass
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 父加載器都沒有加載,則嘗試自己去加載
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
一句話概括:ClassLoader 加載類時(shí),先查看自身是否已經(jīng)加載過該類,如果沒有加載過會首先讓父加載器去加載,如果父加載器無法加載該類時(shí)才會調(diào)用自身的 findClass 方法加載,該邏輯避免了類的重復(fù)加載。所以我們所要實(shí)現(xiàn)的就是把要替換的類可見性提前,這樣類加載器就會優(yōu)先找到修復(fù)過的類。
類的查找過程
//DexClassLoader間接調(diào)用父類findClass方法
//,findClass方法中調(diào)用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
//看DexPathList中的findClass方法,可以看到它是遍歷dexElements數(shù)組,
//到每個(gè)dex文件去尋找當(dāng)前需要的類,找到之后直接返回不往下找了
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
類的查找過程:DexClassLoader通過findClass去查找一個(gè)類,同樣它也是委派給DexPathList的findClass去查找,在DexPathList的findClass中會去遍歷我們上面創(chuàng)建的dexElements數(shù)組,然后在每個(gè)dex中去查找相應(yīng)的類,找到之后就返回,不再向后查找。
熱修復(fù)過程
1、PathClassLoader 作為默認(rèn)的類加載器,也就是第一個(gè) DEX 文件是 PathClassLoader 自動加載的。
2、通過前面的分析來看,我們知道 PathClassLoader 里面的 DEX 文件是存放在一個(gè) Element 數(shù)組中,可以包含多個(gè) DEX 文件,所以我們只需要通過反射獲取 PathClassLoader 中的 DexPathList 中的Element數(shù)組(已加載了第一個(gè)dex包,由系統(tǒng)加載),將要替換的 DEX 文件放置到這個(gè)數(shù)組中去。
3、將兩個(gè)Element數(shù)組合并之后,再將其賦值給 PathClassLoader 的 Element 數(shù)組
Bugly熱更新是基于Tinker的 Bugly 熱更新原理圖如下:


如版本 1.0.0 上線后有bug 啟動1.0.0 版本app的時(shí)候 會把當(dāng)前對應(yīng)的TinkerId上報(bào)到平臺,
修改相關(guān)代碼 執(zhí)行第二步驟 打補(bǔ)丁包后 補(bǔ)丁包里面會有一個(gè)YAPATH.md文件,from 表示我發(fā)布的版本, to 表示我現(xiàn)在打補(bǔ)丁包,意思就是 這個(gè)1.0.0-patch這個(gè)補(bǔ)丁包 是針對于1.0.0-base 來修復(fù)的,
上傳補(bǔ)丁包到配置平臺后,平臺會自動解析YAPATH.md文件, 然后針對性的下發(fā)補(bǔ)丁。