深入探索Android熱修復(fù)技術(shù)原理這本書主要講解了Android的熱修復(fù)中的熱部署,冷部署以及資源和so庫的修復(fù)技巧。全文主要講Sophix應(yīng)對以上四個方面的技術(shù)解析,不管是自家產(chǎn)品還是業(yè)界其他方案的橫縱對比,Sophix技術(shù)目前都是最優(yōu)的。
成都創(chuàng)新互聯(lián)公司成立與2013年,先為余姚等服務(wù)建站,余姚等地企業(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為余姚企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
在事件分發(fā)流中,通過Hook鉤子在事件傳送到終點前截獲并監(jiān)控事件的傳輸,從而處理一些特定干預(yù)事件。
Sophix同時使用了熱啟動的底層替換方案及冷啟動的類加載方案,兩個方案使用的補丁是相同的。優(yōu)先熱啟動。
基本參考InstantRun的實現(xiàn):構(gòu)造一個包含所有新資源的新的AssetManager。并在所有之前引用到原來的AssetManager通過反射替換掉。
Sophix不修改AssetManager的引用,構(gòu)造的補丁包中只包含有新增或有修改變動的資源,在原AssetManager中addAssetPath這個包就可以了。資源包不需要在運行時合成完整包。
本質(zhì)是對native方法的修復(fù)和替換。類似類修復(fù)反射注入方式,將補丁so庫的路徑插入到nativeLibraryDirectories數(shù)據(jù)最前面。
熱修復(fù)不難,
但熱修復(fù)因為大量涉及android底層知識,又因為android本身開源,華為vivo小米幾大廠商都可能修改底層相關(guān)代碼,兼容性困難,所以熱修復(fù)技術(shù)開發(fā)維護難度巨大,人力和時間投入不菲。目前主要有騰訊,阿里等幾家互聯(lián)網(wǎng)大廠因自身剛性需求,實現(xiàn)此功能。
目前熱修復(fù)技術(shù)主要有以下幾家:
騰訊系:
QQ超級補丁
**tinker **
阿里系:
Xposed (不支持Art虛擬機,已廢棄)
Andfix (native hook兼容差,適配機型少)
Android手寫熱修復(fù)(一)--ClassLoader
我們平時編寫的 .java 文件不是可執(zhí)行文件,需要先編譯成 .class 文件才可以被虛擬機執(zhí)行。所謂類加載是指通過 類加載器 把class文件加載到虛擬機的內(nèi)存空間,具體來說是方法區(qū)。類通常是按需加載,即第一次使用該類時才加載。
首先,Java與Android都是把類加載到虛擬機內(nèi)存中,然后由虛擬機轉(zhuǎn)換成設(shè)備識別的機器碼。但是由于二者使用的虛擬機不同,所以在類加載方面也是有所區(qū)別的。Java的虛擬機是JVM,Android的虛擬機是dalvik/art(5.0以后虛擬機是art,是對dalvik的一種升級)。 Java虛擬機運行的是class文件,而Android 虛擬機運行的是dex文件。 dex其實是class文件的集合,是對class文件優(yōu)化的產(chǎn)物,是為了避免出現(xiàn)重復(fù)的class。
從上面的講解中,我們已經(jīng)知道我們平時寫的類是被 類加載器 加載盡虛擬機內(nèi)存才能運行。下面就通過Framework源碼來為大家講解Android中最主要的5個類加載器。
在Activity做個簡單驗證:
結(jié)果:
可以看出系統(tǒng)類由BootClassLoader加載,apk中的類由PathClassLoader加載,PathClassLoader的父類加載器是BootClassLoader。如果暫時不能理解父類加載器是什么,沒關(guān)系,后面講雙親委托機制的時候會理解的。
下面的源碼解析基于 Android SDK API28 ,這幾個類加載器(除了ClassLoader)沒辦法直接在AS上查看源碼,AS搜索到的是反編譯的class的內(nèi)容,是不可信的,為大家推薦一個在線工具查看, 在線查看Android Framework源碼 。
用來加載本地文件系統(tǒng)上的文件或目錄,通常是用來加載apk中我們自己寫的類,而像 Activity.class 這種系統(tǒng)的類不是由它加載。注意:這里,并不像很多網(wǎng)上文章說的那樣只能加載apk,本地的其他目錄的文件也是可以的,這一點我會在后面驗證說明。
也是被用來加載 jar 、apk、dex,通常用來加載未安裝到應(yīng)用中的文件。注意,它需要一個應(yīng)用私有的可寫的目錄來存放優(yōu)化后的dex文件。千萬不要選擇外部存儲路徑,因為這樣可能會導(dǎo)致你的應(yīng)用遭到注入攻擊。
關(guān)于dex文件優(yōu)化,可能很多人還是不理解,水平有限,我簡單解釋一下,
構(gòu)造器參數(shù)解釋:
關(guān)于optimizedDirectory:
1、這是dex優(yōu)化后的路徑,它必須是一個應(yīng)用私有的可寫的目錄否則會存在注入攻擊的風(fēng)險;
2、這個參數(shù)在API 26(8.0)之前是有值的,之后的話,這個參數(shù)已經(jīng)沒有影響了,因為在調(diào)用父構(gòu)造器的時候這個參數(shù)始終為null,也就是說Android 8.0 以后DexClassLoader和PathClassLoader基本一樣的來;
3、在加載app的時候,apk內(nèi)部的dex已經(jīng)執(zhí)行過優(yōu)化了,優(yōu)化之后放在系統(tǒng)目錄/data/dalvik-cache下。
這個構(gòu)造器的關(guān)鍵是初始化了一個DexPathList對象,這個是后面加載class的關(guān)鍵類。
這個構(gòu)造方法等關(guān)鍵是通過 makeDexElements() 方法來獲取Element數(shù)組,這個Element數(shù)組非常關(guān)鍵,后面查找class就會用到它,也是熱修復(fù)的關(guān)鍵點之一。
splitDexPath(dexPath) 方法是把dexPath目錄下的所有文件轉(zhuǎn)換成一個File集合,如果是多個文件的話,會用 : 作為分隔符。
makeDexElements()
小結(jié)一下,這個方法就是把指定目錄下的文件apk/jar/zip/dex按不同的方式封裝成Element對象,然后按順序添加到Element[]數(shù)組中。
DexPathList#loadDexFile()
可以看到 DexFile 最終是調(diào)用了openDexFile、native方法openDexFileNative去打開Dex文件的,如果outputName為空,則自動生成一個緩存目錄,具體來說是 /data/dalvik-cache/xxx@classes.dex 。openDexFileNative這個native方法就不具體分析了,主要是對dex文件進行了優(yōu)化操作,將優(yōu)化后得odex文件通過mmap映射到內(nèi)存中。感興趣的同學(xué)可以參考:
《DexClassLoader和PathClassLoader加載Dex流程》
現(xiàn)在在回頭看看DexClassLoader與PathClassLoader的區(qū)別。DexClassLoader可以指定odex的路徑,而PathClassLoader則采用系統(tǒng)默認(rèn)的緩存路徑,在8.0以后沒有區(qū)別。
ClassLoader是一個抽象類,有3個構(gòu)造方法,最終調(diào)用的還是第一個構(gòu)造方法,主要功能是保存實現(xiàn)類傳入的parent參數(shù),也就是父類加載器。ClassLoader的實現(xiàn)類主要有2個,一個是前面講過的BaseDexClassLoader,另一個是BootClassLoader。
BootClassLoader是ClassLoader的內(nèi)部類,而且繼承了ClassLoader。
這是加載一個類的入口,流程如下:
1、 先檢查這個類是否已經(jīng)被加載,有的話直接返回Class對象;
2、如果沒有加載過,通過父類加載器去加載,可以看出parent是通過遞歸的方式去加載class的;
3、如果所有的父類加載器都沒有加載過,就由當(dāng)前的類加載器去加載。
通常我們自己寫的類是通過當(dāng)前類加載器調(diào)用 findClass 方法去加載的,但是在 ClassLoader 中這是個空方法,具體的實現(xiàn)在它的子類 BaseDexClassLoader 中。
BaseDexClassLoader # findClass
可以看到是通過pathList去查找class的,這個對象其實之前講過,它是在BaseDexClassLoader 的構(gòu)造方法中初始化的,它實際上是一個 DexPathList 對象。
DexPathList # findClass()
對Element數(shù)組遍歷,再通過Element對象的 findClass 方法去查找class,有的話就直接返回這個class,找不到則返回null。 這里可以看出獲取Class是通過DexFile來實現(xiàn)的,而各種類加載器操作的是Dex。Android虛擬機加載的dex文件,而不是class文件。
1、加載一個類是通過雙親委托機制來實現(xiàn)的。
2、如果是第一次加載class,那是通過 BaseDexClassLoader 中的findClass方法實現(xiàn)的;接著進入 DexPathList 中的findClass方法,內(nèi)部通過遍歷Element數(shù)組,從Element對象中去查找類;Element實際上是對Dex文件的包裝,最終還是從dexfile去查找的class。
3、一般app運行主要用到2個類加載器,一個是PathClassLoader:主要用于加載自己寫的類;另一個是BootClassLoader:用于加載Framework中的類;
4、熱修復(fù)和插件化一般是利用DexClassLoader來實現(xiàn)。
5、PathClassLoader和DexClassLoader其實都可以加載apk/jar/dex,區(qū)別是 DexClassLoader 可以指定 optimizedDirectory ,也就是 dex2oat 的產(chǎn)物 .odex 存放的位置,而 PathClassLoader 只能使用系統(tǒng)默認(rèn)位置。但是在8.0 以后二者是沒有區(qū)別的,只能使用系統(tǒng)默認(rèn)的位置了。
這張圖來源于:
Android虛擬機框架:類加載機制
在類加載流程分析中,我們已經(jīng)知道,查找class是通過DexPathList來完成的,實際上DexPathList最終還是遍歷其Element數(shù)組,獲取DexFile對象來加載Class文件。 由于數(shù)組是有序的,如果2個dex文件中存在相同類名的class,那么類加載器就只會加載數(shù)組前面的dex中的class。如果apk中出現(xiàn)了有bug的class,那只要把修復(fù)的class打包成dex文件并且放在 DexPathList 中Element數(shù)組`的前面,就可以實現(xiàn)bug修復(fù)了 。下一篇為大家?guī)淼氖謱憻嵝迯?fù)。
Android類加載機制的細(xì)枝末節(jié)
從JVM到Dalivk再到ART(class,dex,odex,vdex,ELF)
類加載機制系列2——深入理解Android中的類加載器
Android 熱修復(fù)核心原理,ClassLoader類加載
針對Android平臺,Dexposed支持函數(shù)級別的在線熱更新,例如對已經(jīng)發(fā)布在應(yīng)用市場上的宿主APK,當(dāng)我們從crash統(tǒng)計平臺上發(fā)現(xiàn)某個函數(shù)調(diào)用有bug,導(dǎo)致經(jīng)常性crash,這時,可以在本地開發(fā)一個補丁APK,并發(fā)布到服務(wù)器中,宿主APK下載這個補丁APK并集成后,就可以很容易修復(fù)這個crash。
Dexposed是基于久負(fù)盛名的開源Xposed框架實現(xiàn)的一個Android平臺上功能強大的無侵入式運行時AOP框架。
Dexposed的AOP實現(xiàn)是完全非侵入式的,沒有使用任何注解處理器,編織器或者字節(jié)碼重寫器。集成Dexposed框架很簡單,只需要在應(yīng)用初始化階段加載一個很小的JNI庫就可以,這個加載操作已經(jīng)封裝在DexposedBridge函數(shù)庫里面的canDexposed函數(shù)中,源碼如下所示:
/**
* Check device if can run dexposed, and load libs auto.
*/
public synchronized static boolean canDexposed(Context context) {
if (!DeviceCheck.isDeviceSupport(context)) {
return false;
}
//load xposed lib for hook.
return loadDexposedLib(context);
}
private static boolean loadDexposedLib(Context context) {
// load xposed lib for hook.
try {
if (android.os.Build.VERSION.SDK_INT 19){
System.loadLibrary("dexposed_l");
} else if (android.os.Build.VERSION.SDK_INT == 10
|| android.os.Build.VERSION.SDK_INT == 9 ||
android.os.Build.VERSION.SDK_INT 14){
System.loadLibrary("dexposed");
}
return true;
} catch (Throwable e) {
return false;
}
}
Dexposed實現(xiàn)的hooking,不僅可以hook應(yīng)用中的自定義函數(shù),也可以hook應(yīng)用中調(diào)用的Android框架的函數(shù)。Android開發(fā)者將從這一點得到很多好處,因為我們嚴(yán)重依賴于Android SDK的版本碎片化。
因為對手機傷害大。android手機熱修復(fù)不能百分百用戶修復(fù)成功,手機影響極大而且手機很容易出現(xiàn)bug,所以手機廠商不允許熱修復(fù)。