Android熱修復原理簡介
今天看到塞爾維亞總統(tǒng)在全國電視直播中說到期揪,只有中國才能救我們的時候,作為中國人的那種驕傲油然而生摇幻,很幸運能見證中國的崛起和強大横侦,這才是大國當擔的樣子。
閑話少說绰姻,今天準備寫一篇關(guān)于Android熱修復的東西
熱修復四大框架
首先我們來對看一下主流框架對于熱修復的對比圖,了解一下各大廠商用的框架對比引瀑。熱補丁方案有很多狂芋,其中比較出名的有騰訊Tinker、阿里的AndFix憨栽、美團的Robust以及QZone的超級補丁方案帜矾,下面是他們的對比
騰訊Tinker:Tinker通過計算對比指定的Base Apk中的dex與修改后的Apk中的dex的區(qū)別,補丁包中的內(nèi)容即為兩者差分的描述屑柔。運行時將Base Apk中的dex與補丁包進行合成屡萤,重啟后加載全新的合成后的dex文件。
特點:重啟生效掸宛、反射死陆、類加載、DexDiff
QQ的Qzone:QQ空間基于的是dex分包方案唧瘾。把BUG方法修復以后措译,放到一個單獨的dex補丁文件,讓程序運行期間加載dex補丁饰序,執(zhí)行修復后的方法领虹。如何做到這一點?在Android中所有我們運行期間需要的類都是由ClassLoader(類加載器)進行加載求豫。因此讓ClassLoader加載全新的類替換掉出現(xiàn)Bug的類即可完成熱修復塌衰。
特點:重啟生效诉稍、反射、類加載
美團Robust:對每個函數(shù)都在編譯打包階段自動的插入了一段代碼最疆。類似于代理均唉,將方法執(zhí)行的代碼重定向到其他方法中。
特點:即時生效肚菠、注解舔箭、插樁、代理
阿里AndFix:在native動態(tài)替換java層的方法蚊逢,通過native層hook java層的代碼层扶。
特點:即時生效、不能替換類烙荷,只是通過改變Native層的指針改變所指向的方法镜会,從而完成對方法的修復
以上是各大平臺使用熱修復方案的優(yōu)缺點,有些地方可能有些難以理解终抽,這篇文章將著重介紹Qzone的原理和具體實現(xiàn)戳表,其它方案讀者可以自行研究,此處只做簡單的介紹昼伴。
QQ空間Qzone原理
在介紹Qzone的實現(xiàn)原理之前匾旭,需要向大家介紹這么幾個知識點:
-
類加載機制 classloader的原理
我們知道任何一個類的class對象都會對應一個classloader,表示該類被哪個類加載器加載圃郊,Android原生api為我們提供了二種ClassLoader的抽象子類价涝,分別為BootClassLoader,BaseClassLoader
BootClassLoader用于加載Android Framework層的class文件持舆,例于Activity.class等等
BaseDe'xClassLoadexer下面又有兩個子類色瘩,PathClassLoader,DexClassLoder
PathClassLoader用于加載自己寫的類逸寓,或者第三方庫里面的類居兆,包括android自己開發(fā)的第三方庫
DexClassLoder 和PathClassLoader一樣,都是用來加載class文件
其實兩者并沒有太大區(qū)別竹伸,只是構(gòu)造方法不同而已泥栖,谷歌的意思是系統(tǒng)的類用pathclassloader,而我們用戶自己寫的類用DexClassLoder佩伤,但其實兩者可以互相替換使用聊倔,只不過DexClassLoder比pathclassloader的構(gòu)造方法多了一個參數(shù),而這個參數(shù)只是用來保存我們的odex文件的目錄生巡,且在android更高的版本耙蔑,這個參數(shù)也被棄用,被統(tǒng)一保存到系統(tǒng)的目錄中孤荣。
這些類加載器有一個共同的特性甸陌,在加載完一個類的class文件以后须揣,不會再去加載相同的class文件,而我們就是利用這種機制钱豁,去實現(xiàn)熱修復耻卡。
在應用程序啟動的時候,所有類的class文件牲尺,會被添加到一個Element的數(shù)組中卵酪,classloader有序的遍歷這個數(shù)組,當遇見加載過重復的類時谤碳,就不會再去加載溃卡,所以我們只要想辦法,幫我們要修復的class文件添加到這個集合的最前面蜒简,也就完成了熱修復功能瘸羡。
雙親委托機制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
這是classloader加載類時候的源碼,findLoadedClass相當于緩存搓茬,如果之前加載過可以直接加載出來犹赖。假設(shè)我們程序重新啟動,代碼會執(zhí)行到 c = parent.loadClass(name, false); 查看源碼可知parent為classloader內(nèi)部維護的一個成員變量classloader parent卷仑,這里優(yōu)先讓parent加載類峻村,如果parent沒有找到,自己再去找系枪,其實這里面有點類似裝飾者模式雀哨,我們思考一個問題,在這個內(nèi)部維護的parent內(nèi)部是不是也有一個相同的classloader 私爷,然后在查找這個name的時候,又會委托parent內(nèi)部維護的classloader 去做膊夹,直到找不到為止衬浑,就自己來找。我們把這種機制稱之為雙親委托機制放刨。永遠先讓父加載器加載工秩。總結(jié)入下:
某個類加載器在加載類時进统,首先將加載任務委托給父 - 類加載器助币,依次遞歸,如果父類加載器可以完成類加載任務螟碎,就成功返回眉菱;只有父類加載器無法完成此加載任務或者沒有父類加載器時,才自己去加載掉分。
那么為什么會有這個機制呢俭缓,
1克伊、避免重復加載,當父加載器已經(jīng)加載了該類的時候华坦,就沒有必要子ClassLoader再加載一次愿吹。且只有一個classLoader就能加載出來系統(tǒng)所有的class對象
2、安全性考慮惜姐,防止核心API庫被隨意篡改犁跪。 (假設(shè)我創(chuàng)建一個String類,如果沒有這種機制歹袁,回導致我們的String類把系統(tǒng)的String替換掉)
掌握以上兩點基礎(chǔ)知識坷衍,我們再來看看classloader是如何去加載一個類的宇攻。我們已經(jīng)了解了惫叛,如果我們自己寫一個類是會被PathClassLoader加載的,所以parent.loadClass(name, false)是注定找不到我們要修復的類嘉涌,然后我們看看findClass的邏輯仑最。PathClassLoader沒有實現(xiàn)這個方法帆喇,我們來看他的父類BaseDexClassLoader的findclass
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
在findclass里面警医,又是通過pathList來查找,所以我們可以繼續(xù)查看pathList.finClass做了什么
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
在DexPathList內(nèi)部坯钦,又是通過element來findClass预皇,所以我們最終只要鎖定Element這個數(shù)組即可。系統(tǒng)會把我們所有dex文件婉刀,加載到Element數(shù)組中吟温,然后有序遍歷,而我們要想給一個類打補丁突颊,就必須要保證這個補丁類的dex文件在錯誤類dex文件之前加載鲁豪,而實現(xiàn)步驟就是在這個數(shù)組最開始的位置插入這個打了補丁的dex文件即可。(因為數(shù)組大小固定律秃,為了避免數(shù)組角標越界爬橡,我們需要替換這個數(shù)組而不是插入)
所以總結(jié)一下,想要做到熱修復棒动,需要做到如下幾步:
獲取到當前應用的PathClassloader;
反射獲取到DexPathList屬性對象pathList;
反射修改pathList的dexElements
3.1 把補丁包patch.dex轉(zhuǎn)化為Element[] (patch)
3.2 獲得pathList的dexElements屬性(old)
3.3 patch+old合并糙申,并反射賦值給pathList的dexElements
問題:QQ空間兼容問題
https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a