Android手寫熱修復(fù)(一)--ClassLoader
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項目有:域名申請、網(wǎng)站空間、營銷軟件、網(wǎng)站建設(shè)、泗洪網(wǎng)站維護(hù)、網(wǎng)站推廣。
我們平時編寫的 .java 文件不是可執(zhí)行文件,需要先編譯成 .class 文件才可以被虛擬機(jī)執(zhí)行。所謂類加載是指通過 類加載器 把class文件加載到虛擬機(jī)的內(nèi)存空間,具體來說是方法區(qū)。類通常是按需加載,即第一次使用該類時才加載。
首先,Java與Android都是把類加載到虛擬機(jī)內(nèi)存中,然后由虛擬機(jī)轉(zhuǎn)換成設(shè)備識別的機(jī)器碼。但是由于二者使用的虛擬機(jī)不同,所以在類加載方面也是有所區(qū)別的。Java的虛擬機(jī)是JVM,Android的虛擬機(jī)是dalvik/art(5.0以后虛擬機(jī)是art,是對dalvik的一種升級)。 Java虛擬機(jī)運(yùn)行的是class文件,而Android 虛擬機(jī)運(yùn)行的是dex文件。 dex其實是class文件的集合,是對class文件優(yōu)化的產(chǎn)物,是為了避免出現(xiàn)重復(fù)的class。
從上面的講解中,我們已經(jīng)知道我們平時寫的類是被 類加載器 加載盡虛擬機(jī)內(nèi)存才能運(yùn)行。下面就通過Framework源碼來為大家講解Android中最主要的5個類加載器。
在Activity做個簡單驗證:
結(jié)果:
可以看出系統(tǒng)類由BootClassLoader加載,apk中的類由PathClassLoader加載,PathClassLoader的父類加載器是BootClassLoader。如果暫時不能理解父類加載器是什么,沒關(guān)系,后面講雙親委托機(jī)制的時候會理解的。
下面的源碼解析基于 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文件進(jìn)行了優(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虛擬機(jī)加載的dex文件,而不是class文件。
1、加載一個類是通過雙親委托機(jī)制來實現(xiàn)的。
2、如果是第一次加載class,那是通過 BaseDexClassLoader 中的findClass方法實現(xiàn)的;接著進(jìn)入 DexPathList 中的findClass方法,內(nèi)部通過遍歷Element數(shù)組,從Element對象中去查找類;Element實際上是對Dex文件的包裝,最終還是從dexfile去查找的class。
3、一般app運(yùn)行主要用到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ī)框架:類加載機(jī)制
在類加載流程分析中,我們已經(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類加載機(jī)制的細(xì)枝末節(jié)
從JVM到Dalivk再到ART(class,dex,odex,vdex,ELF)
類加載機(jī)制系列2——深入理解Android中的類加載器
Android 熱修復(fù)核心原理,ClassLoader類加載
因為對手機(jī)傷害大。android手機(jī)熱修復(fù)不能百分百用戶修復(fù)成功,手機(jī)影響極大而且手機(jī)很容易出現(xiàn)bug,所以手機(jī)廠商不允許熱修復(fù)。
深入探索Android熱修復(fù)技術(shù)原理這本書主要講解了Android的熱修復(fù)中的熱部署,冷部署以及資源和so庫的修復(fù)技巧。全文主要講Sophix應(yīng)對以上四個方面的技術(shù)解析,不管是自家產(chǎn)品還是業(yè)界其他方案的橫縱對比,Sophix技術(shù)目前都是最優(yōu)的。
在事件分發(fā)流中,通過Hook鉤子在事件傳送到終點前截獲并監(jiān)控事件的傳輸,從而處理一些特定干預(yù)事件。
Sophix同時使用了熱啟動的底層替換方案及冷啟動的類加載方案,兩個方案使用的補(bǔ)丁是相同的。優(yōu)先熱啟動。
基本參考InstantRun的實現(xiàn):構(gòu)造一個包含所有新資源的新的AssetManager。并在所有之前引用到原來的AssetManager通過反射替換掉。
Sophix不修改AssetManager的引用,構(gòu)造的補(bǔ)丁包中只包含有新增或有修改變動的資源,在原AssetManager中addAssetPath這個包就可以了。資源包不需要在運(yùn)行時合成完整包。
本質(zhì)是對native方法的修復(fù)和替換。類似類修復(fù)反射注入方式,將補(bǔ)丁so庫的路徑插入到nativeLibraryDirectories數(shù)據(jù)最前面。
注意:在5.0之前會有這個問題,5.0之后沒有了
我們進(jìn)行插樁的時機(jī),便是上圖中javac之后,dx之前。 另外,任何一個Task,都有input元素和output元素,以及可以設(shè)置doFirst閉包,表示執(zhí)行任務(wù)之前先執(zhí)行一段邏輯,設(shè)置doLast,表示執(zhí)行任務(wù)執(zhí)行之后再執(zhí)行一段邏輯。
我們的思路是 在java變成class之后,在class變成 dex之前,將class進(jìn)行ASM插樁。所以,我們要找的 gradle task 是 : transformClassesWithDexBuilderForRelease 或者 transformClassesWithDexBuilderForDebug 給它重寫doFirst。 也可以 找到 gradle task : compileReleaseJavaWithJavac 或者 compileDebugJavaWithJavac. 給它重寫 doLast。效果相同。
jar文件的插樁:
本文是在Android8.1.0上進(jìn)行的源碼分析,首先明確幾個classloader的定義。
dexFile相當(dāng)于一個classes.dex,而每一個dexFile也是一個Element,熱修復(fù)的補(bǔ)丁包一定要插到classes.dex中的最前面的一個dex開始查找,因為dexFile是從classes.dex classes2.dex依次向后進(jìn)行查找的.前面如果找到就不會找后面的類了。
反射工具類:
熱修復(fù):
成功使用: