Android熱修復(fù)框架AndFix原理解析及使用

一、前言
最近騰訊弄出一個Tinker熱修復(fù)框架,那么本文先不介紹這個框架,先來介紹一下阿里的一個熱修復(fù)框架AndFix,這個框架出來已經(jīng)很長時間了,但是看網(wǎng)上沒有太多非常詳細(xì)的講解,這里就來做一次分析。正好項目中要使用到。首先這個框架是開源的:https://github.com/alibaba/AndFix 其實在最早的時候我已經(jīng)分析了阿里的另外一個熱修復(fù)框架:Dexposed框架,還不了解的同學(xué)可以點擊這里查看:Dexposed框架原理解析以及使用 當(dāng)時介紹這個框架的時候發(fā)現(xiàn)他的實現(xiàn)原理很簡單:


他的思想完全來源于Xposed框架,完美詮釋了AOP編程,這里用到最核心的知識點就是在native層獲取到指定方法的結(jié)構(gòu)體,然后改變他的nativeFunc字段值,而這個值就是可以指定這個方法對應(yīng)的native函數(shù)指針,所以先從Java層跳到native層,改變指定方法的nativeFunc值,然后在改變之后的函數(shù)中調(diào)用Java層的回調(diào)即可。實現(xiàn)了方法的攔截功能。
二、源碼分析
那么本文介紹的AndFix框架相對于Dexposed框架來說又有什么區(qū)別呢?其實區(qū)別就在于AndFix框架更加輕便好用,在進(jìn)行熱修復(fù)的過程中更加方便了。當(dāng)然這個優(yōu)點在后面的Tinker框架也是能提現(xiàn)出來的。這個框架的原理是:直接在native層進(jìn)行方法的結(jié)構(gòu)體信息對換,從而實現(xiàn)完美的方法新舊替換,從而實現(xiàn)熱修復(fù)功能。下面通過分析他的源碼來看他的具體實現(xiàn),下載完源碼之后導(dǎo)入工程:

這里可以看到,因為在native層需要替換新舊方法結(jié)構(gòu)體信息,所以這里肯定要做的工作就是虛擬機的兼容問題,這里做了art和dalvik的分開處理邏輯,下面來看一下這個框架的基本使用規(guī)則:

用法還是很簡單的,這里的修復(fù)包是直接放在本地的,在實際操作中會從網(wǎng)上去下載。下面就開始分析源碼,這里有一個主要的類就是PatchManager

第一、PatchManager類初始化
在這個類的構(gòu)造方法中做了兩件事,一件事是初始化AndFixManager類,一件事是創(chuàng)建修復(fù)包存放的沙盒目錄。這里先來看第二件事創(chuàng)建沙盒目錄:


可以看到這個目錄是:/data/data/xxx/files/apatch/xxx.apatch

當(dāng)我們從網(wǎng)上下載好修復(fù)包apatch文件之后,會調(diào)用addPatch方法,這時候會把修復(fù)包復(fù)制到這個地方,以后再次啟動時就會遍歷這個目錄加載apatch文件。

第二、AndFixManager的初始化
下面繼續(xù)來看AndFixManager初始化操作:


在這個初始化中也是干了兩件事:一件事是判斷當(dāng)前環(huán)境是否支持熱修復(fù),一件事是初始化修復(fù)包安全校驗的工作,先來看一下判斷是否支持操作:

這里支持的條件是:非YunOS系統(tǒng),Android2.3-7.0系統(tǒng)版本,熱修復(fù)native層設(shè)置是否成功。這里我們看到應(yīng)該值得關(guān)心的是setup操作,這個操作其實是native層進(jìn)行的,可以直接看一下具體代碼:

這里主要做了一些初始化操作,獲取一些函數(shù)指針,準(zhǔn)備后續(xù)的replaceMethod函數(shù)中使用:
1、在libdvm.so動態(tài)獲取dvmDecodeIndirectRef函數(shù)指針和獲取dvmThreadSelf函數(shù)指針。2、調(diào)用dest的 Method.getDeclaringClass方法獲取method的類對象clazz。

繼續(xù)回到AndFixManager的初始化中的第二件事:修復(fù)包安全校驗工作,其實這里只是做了初始化操作,而真正校驗的工作在后面,主要是通過比對應(yīng)用的簽名和修復(fù)包的簽名信息。后面再看吧!

第三:PatchManager的初始化操作


這里的初始化做了一件事:是判斷當(dāng)前PatchManager的版本號是否發(fā)生變化,如果發(fā)生變化就清空本地所有的修復(fù)包。如果沒有變化,直接調(diào)用初始化修復(fù)包方法

這個方法其實就是我們上面提到的邏輯,會遍歷沙盒中修復(fù)包目錄中所有的修復(fù)包文件,然后把它添加到修復(fù)列表中。

第四:PatchManager的添加修復(fù)包操作
這里提供了兩個添加修復(fù)包的方法,一個是接受文件樣式的參數(shù):


這個方法就是上面的那個initPatchs方法中調(diào)用的地方,這里會把目錄下所有的修復(fù)包文件加到列表中。這里來看一下另外一個主要類Patch的初始化操作:

這個初始化的主要工作就是通過JarFile類解析修復(fù)包文件,讀取他的META-INF\PATCH.MF文件內(nèi)容,獲取需要修復(fù)類的名稱,多個修復(fù)類之間用逗號分隔,類似于這樣的樣式:

這里的Utils類就是我們需要修復(fù)的方法所屬的類名,但是這里他又做了處理就是在每個類的后面加了后綴:_CF。這里分析完了所有需要修復(fù)的類名之后保存到一個列表中,后面會通過修復(fù)包名稱獲取到他的修復(fù)類名稱列表。

還有一個添加修復(fù)包文件的方法,接受的是文件路徑參數(shù):


這個方法接受的是修復(fù)包文件路徑,首先會把這個文件拷貝到上面提到的沙盒目錄中以便下次進(jìn)行遍歷操作,拷貝之后繼續(xù)調(diào)用上面的addPatch方法添加到列表中,最后就在調(diào)用加載修復(fù)包操作了。
注意:
第一個方法接受文件樣式的方法其實是需要結(jié)合上面的initPatchs方法一起使用,他調(diào)用的場景是:本地沙盒目錄中已經(jīng)有了修復(fù)包文件,并且版本號沒有發(fā)生變化,這樣每次啟動程序的時候就會調(diào)用初始化操作,在這里會遍歷沙盒目錄中所有的修復(fù)包文件,然后調(diào)用這個方法添加到全局文件列表中。
第二個方法接受的是文件路徑樣式,這個方法使用的場景是版本號發(fā)生變化,或者是本地沙盒中沒有修復(fù)包文件。比如第一次操作的時候,會從網(wǎng)絡(luò)上下載修復(fù)包文件,下載成功之后會把這個文件路徑通過這個方法調(diào)用即可,執(zhí)行完之后也會主動調(diào)用加載修復(fù)包的操作了,比如這里第一次在SD卡中放了一個修復(fù)包文件:

只要調(diào)用了這段代碼之后就可以走完了所有的流程了:拷貝修復(fù)包到沙盒目錄中,加載修復(fù)包文件。

第五:PatchManager的加載修復(fù)包操作


這個方法有兩個地方會調(diào)用到:一個是上面提到的那個接受修復(fù)包路徑的addPatch方法,一個是調(diào)用完接受修復(fù)文件類型的addPatch方法之后手動調(diào)用一次,類似于這樣:

這個方法內(nèi)部主要是通過Patch類獲取修復(fù)包所有的修復(fù)類名稱,之前已經(jīng)介紹了Patch類的初始化操作,在哪里會解析修復(fù)包的MF文件信息,獲取到修復(fù)包需要修復(fù)的類名然后保存到列表中,這里就通過getClasses方法來獲取指定修復(fù)包名稱對應(yīng)的修復(fù)類名稱列表,然后在調(diào)用AndFixManager的fix方法即可,下面再來看一下AndFixManager的fix方法的實現(xiàn)邏輯:

這個方法有點長,而且內(nèi)容也比較多,這里主要做了這么幾件事:
第一件事:使用上面初始化完成的校驗類進(jìn)行修復(fù)包的校驗工作,這里的校驗就是比對修復(fù)包的簽名和應(yīng)用的簽名是否一致:

這個具體實現(xiàn)邏輯不用介紹了,大家可以下載源碼自己分析。
第二件事:使用DexFile和自定義類加載器來加載修復(fù)包文件
這個其實和使用DexClassLoader加載原理類似,而且DexClassLoader內(nèi)部的加載邏輯也是使用了DexFile來進(jìn)行操作的,而這里為什么要進(jìn)行加載操作呢?因為我們需要獲取修復(fù)類中需要修復(fù)的方法名稱,而這個方法名稱是通過修復(fù)方法的注解來獲取到的,所以咋們得先進(jìn)行類的加載然后獲取到他的方法信息,最后通過分析注解獲取方法名,這里用的是反射機制來進(jìn)行操作的。

這個加載完類之后就會繼續(xù)調(diào)用fixClass方法,再來看一下fixClass方法實現(xiàn):

這里主要是通過反射獲取指定類名需要修復(fù)類中的所有方法類型,然后在獲取到他的注解信息,上面已經(jīng)分析了通過DexFile加載修復(fù)包文件,然后在加載上面Patch類中的getClasses方法獲取到的修復(fù)類名稱列表來進(jìn)行類的加載,然后在用反射機制獲取類中所有的方法對應(yīng)的注解信息,通過注解信息獲取指定修復(fù)的方法名稱,看一下這個注解的定義:


這里提供了兩個方法,一個是獲取當(dāng)前類名稱,一個是獲取當(dāng)前方法名稱,可以看一下具體事例:

上面解析完注解信息之后獲取到了方法名稱,緊接著就調(diào)用了replaceMethod方法開始了方法的替換操作

這里還會做一件事就是通過上面得到的修復(fù)新的方法信息以及需要修復(fù)的舊方法名稱來操作,不過這里得先獲取到舊方法類型,可以看到修復(fù)的新舊方法的簽名必須一致,所謂簽名就是方法的名稱,參數(shù)個數(shù),參數(shù)類型都必須一致,不然這里就報錯的。進(jìn)而也修復(fù)不了了。最后在調(diào)用了AndFix的addReplaceMethod方法進(jìn)行native層的修復(fù)工作:

這里會做虛擬機的區(qū)分處理,但是他們大致的處理邏輯都是一致的,這里來看一下dalvik的處理機制:

這里的操作也是非常簡單的,主要是通過上層傳遞過來的新舊方法類型對象,通過JNIEnv的FromReflectedMethod方法獲取對應(yīng)的方法結(jié)構(gòu)體信息,然后將其信息進(jìn)行替換即可,這里可以看到替換的信息也是非常多的,而且也看到了我們之前介紹Dexposed框架用到的一個字段值nativeFunc,這個就是指定這個Java方法對應(yīng)的native方法。但是在這之前也會看到有一段代碼是用來獲取修復(fù)方法的類信息的,這里主要是用來做修復(fù)方法的類初始化操作,在之前我們看setup方法的時候知道,那里做了這么兩件事:
1、在libdvm.so動態(tài)獲取dvmDecodeIndirectRef函數(shù)指針和獲取dvmThreadSelf函數(shù)指針。2、調(diào)用dest的 Method.getDeclaringClass方法獲取method的類對象clazz。
然后在這里就開始獲取修復(fù)方法對應(yīng)的類信息,通過調(diào)用方法的getDeclaringClass獲取方法對應(yīng)的類對象clazz,然后在調(diào)用dvm方法獲取到對應(yīng)的類結(jié)構(gòu)體信息ClassObject,最后在設(shè)置他的狀態(tài)信息標(biāo)記這個類已經(jīng)初始化完畢了。
注意:
這里可以看到通過一個類對象clazz類型可以獲取到對應(yīng)的結(jié)構(gòu)體信息ClassObject,這個操作也是非常實用的,因為這個結(jié)構(gòu)信息中有一個字段pDvmDex值,而這個值就是DvmDex結(jié)構(gòu)體信息也就是底層對應(yīng)的dex文件信息,所以說我們可以通過一個類信息得到他對應(yīng)的dex文件信息。

三、流程總結(jié)
到這里就講解完了整個框架的所有技術(shù)點了,上面可能說的有點亂,下面在來大體總結(jié)一下,首先來看一張簡單的流程圖信息(圖片有點大,可以下載看高清大圖):

第一、Patch類負(fù)責(zé)解析每個修復(fù)包apatch文件信息,獲取所有需要修復(fù)的類名
這個類的初始化操作中會通過傳遞進(jìn)來的修復(fù)包文件,使用JarFile類進(jìn)行文件解析,讀取他的META-INF\PATCH.MF文件信息,主要通過讀取Patch-Classes字段值來獲取需要修復(fù)的類名稱,多個類名稱之間用逗號分隔,而且每個類名稱都有一個后綴:_CF。解析完成之后就會保存到一個用修復(fù)包名稱作為key的HashMap中,后面會通過修復(fù)包名稱參數(shù)來調(diào)用getClasses方法獲取對應(yīng)修復(fù)的類名稱列表。
第二、PatchManager負(fù)責(zé)管理多個Patch類也就是多個修復(fù)包信息
主要方法包括初始化,添加修復(fù)包,加載修復(fù)包,這個類是提供給外界調(diào)用的一個入口類,這里有兩種方式調(diào)用:
一種方式是先調(diào)用init方法進(jìn)行初始化,在這個初始化方法中會判斷當(dāng)前的版本號,如果版本號發(fā)生變化就會清空本地所有的修復(fù)包文件,如果沒有變化就加在所有的本地修復(fù)包文件,而這個本地目錄就是沙盒中存放修復(fù)包文件的目錄:/data/data/xxx/apatch/xxx.apatch,然后在調(diào)用loadPatch方法進(jìn)行修復(fù)包的加載工作。
一種方式是本地沒有修復(fù)包文件,也就是第一次操作的時候可能需要從服務(wù)器下載修復(fù)包文件,這時候會把下載下來的修復(fù)包文件路徑通過調(diào)用addPatch方法進(jìn)行添加操作,而這里的添加操作包括了:先把修復(fù)文件拷貝到上面的沙盒修復(fù)包目錄中,然后在調(diào)用loadPatch方法進(jìn)行加載工作。
第三、AndFix類主要是和native層交互直接替換方法
這個類主要就是幾個native方法用來和底層進(jìn)行交互的操作,而這些方法都是會被AndFixManager進(jìn)行調(diào)用的。
第四、AndFixManager類主要是負(fù)責(zé)管理AndFix類
主要方法包括加載每個修復(fù)包中需要修復(fù)的類,解析出每個類的注解信息獲取該類需要修復(fù)的方法名稱,初始化的時候會進(jìn)行修復(fù)包的校驗工作,主要通過對比修復(fù)包和應(yīng)用的簽名信息。所以可以知道每個修復(fù)包是需要進(jìn)行簽名操作的,然后他的fix方法會使用DexFile類進(jìn)行加載修復(fù)包文件,調(diào)用Patch的getClasses方法獲取到所有需要修復(fù)的類名稱進(jìn)行加載操作。然后在調(diào)用fixClass方法,在這個方法中主要通過遍歷修復(fù)類中所有指定MethodReplace注解信息的方法信息,然后在調(diào)用replaceMethod方法進(jìn)行替換操作,而在這個方法中也會通過新方法的Method類型和注解信息中需要修復(fù)方法的名稱來得到舊方法的Method類型,最終調(diào)用AndFix的native方法replaceMethod進(jìn)行替換操作,所以這里可以看到替換的新舊方法的簽名信息必須一致,不然無效,也就是方法的名稱,參數(shù)個數(shù),參數(shù)類型必須保持一致才可以。

第五、Native層方法
在native層中會做art和dalvik虛擬機的區(qū)分處理工作,他們大致的邏輯都是一致的:
dalvik 模式下的Java hook1、在libdvm.so動態(tài)獲取dvmDecodeIndirectRef函數(shù)指針和獲取dvmThreadSelf函數(shù)指針。2、調(diào)用dest的 Method.getDeclaringClass方法獲取method的類對象clazz。3、調(diào)用dvmDecodeIndirectRef方法,獲取clazz的ClassObject4、通關(guān) env->FromReflectedMethod方法獲取dest的Method結(jié)構(gòu)體函數(shù)的指針5、替換method結(jié)構(gòu)體的成員數(shù)據(jù)art模式下的java hook1、art模式中,我們直接通過 env->FromReflectedMethod獲取到ArtMethod函數(shù)指針。2、然后直接替換ArtMethod結(jié)構(gòu)體的成員數(shù)據(jù)指針
******四、框架使用案例
*
上面介紹完了原理,下面如果不用案例來做分析,那都是白扯淡,這里我們就用一個簡單的案例來進(jìn)行實際操作一下,而且在這個過程中會發(fā)現(xiàn)一個神奇的工具apatch,這里的例子很簡單,本地定義一個獲取版本號的方法:


這時候我們得到這個值,然后顯示在界面上,然后開始出release包,這里直接用eclipse構(gòu)造簽名文件(這個簽名文件要記得保存好,后面會使用到)出包了。等包上線發(fā)布之后,突然發(fā)現(xiàn)這個版本號錯了,應(yīng)該是1.0.2,那么這時候就需要進(jìn)行熱修復(fù)了,操作很簡單:
第一步:修改這個方法返回值為1.0.2

第二步:繼續(xù)使用上面的簽名文件進(jìn)行簽名得到了一個修復(fù)之后的apk包

第三步:使用神器apatch進(jìn)行線上發(fā)布的release包和這次修復(fù)的fix包進(jìn)行比對,獲取到修復(fù)文件apatch
java -jar apkpatch.jar -f app-release-fix.apk -t app-release-online.apk -o C:\Users\jiangwei1-g\Desktop\apkpatch-1.0.3 -k jiangwei.keystore -p 123456 -a jiangwei -e 123456
這里在使用命令的時候需要用到簽名文件,因為在前面分析代碼的時候知道會做修復(fù)包的簽名驗證。這里得到了一個修復(fù)包文件如下:


而且會產(chǎn)生一個diff.dex文件和smali文件夾,這個就是修復(fù)類的文件對應(yīng)的dex文件和smali代碼:

而我們用壓縮軟件可以打開apatch文件看看:

可以看到這里的classes.dex文件其實就是上面的diff.dex文件,只是這里更像是Android中的apk文件目錄格式,同樣有一個META-INF目錄,這里存放了簽名文件以及需要修復(fù)類信息的PATCH.MF文件:

簽名文件就不多說了,來看一下PATCH_MF文件信息:

Patch_Classes字段包含了需要修復(fù)的類的名稱信息了。

第四步:這里為了演示方便,直接把上面的修復(fù)文件拷貝到sd卡中,然后調(diào)用PatchManager的addPatch方法:


第五步:運行程序



這時候可以發(fā)現(xiàn)版本號已經(jīng)修復(fù)成了1.0.2了。

五、apatch工具原理解析
上面看到案例使用比較簡單,但是看到有一個比較牛逼的工具就是apatch,可以生成有方法變動的類所在的dex文件,那下面就來一起看看他的實現(xiàn)原理,沒找到源碼,直接使用jd-gui查看apatch.jar文件了:


這里的核心代碼就是這部分,會把有方法變動的類信息列表對象DexBackedClassDef借助baksmali類寫入到smali文件中,然后在借助DexBuilder和SmaliMod類把smali類變成dex文件,也就是最終的diff.dex文件了。那么下面在來看一下這個變動的DexBackedClassDef類列表信息如何得到的:

在這里使用DexBackedDexFile類進(jìn)行加載新舊的dex文件,然后開始比對具體方法實現(xiàn)變動情況,主要是方法:compareMethod的實現(xiàn):

這里會調(diào)用方法的getImplementation方法來判斷新舊方法的實現(xiàn)發(fā)生變動了,如果有就把當(dāng)前的類對象加入變動列表中即可。
所以從這里可以看到這里其實是完全借助了第三方的功能:可以把dex變成smali文件的baksmali工具包、可以把smali變成dex文件的smali工具包。而這兩個工具包的源碼之前在介紹apktool反編譯工具的時候說到了,想查看源碼的同學(xué)可以查看這篇文章:反編譯利器apktool的源碼解析。從這里可以看到,我們后續(xù)再處理dex,smali等文件格式的時候這兩個工具包用的非常多。

六、框架技術(shù)總結(jié)
到此此次修復(fù)操作就完成了。我們的講解工作和案例演示工作也完成了,下面來總結(jié)一下這個框架的知識點,不過先來看一張大圖(可以點擊下載查看高清大圖):


第一、核心技術(shù)點
從上面可以看到AndFix框架的技術(shù)點主要包括:
1、使用apatch工具生成修復(fù)包文件,主要借助baksmali和smali工具包實現(xiàn)
2、Java層傳遞新舊方法類型對象,到native層獲取其對應(yīng)的結(jié)構(gòu)體信息實現(xiàn)完美替換新舊方法結(jié)構(gòu)信息
第二、優(yōu)點和局限性
優(yōu)點:從上面可以看到這個框架的優(yōu)點在于輕巧便捷,集成成本低,維護(hù)性強。
局限性:從上面的代碼分析可以看到這個框架的局限性還是很多的,特別是他只能修復(fù)對應(yīng)已經(jīng)存在的方法,比如現(xiàn)在我想增加一個方法肯定不行的,如果想給修復(fù)方法增加參數(shù)信息也是不可以的,這個局限性就非常大了。還有一個局限性就是只能進(jìn)行代碼修復(fù),資源是無法做到的。所以從這里可以看到這個框架更偏重于方法的熱修復(fù)操作。

項目下載地址:http://download.csdn.net/detail/jiangwei0910410003/9678441
工具下載地址:http://download.csdn.net/detail/jiangwei0910410003/9678885

七、總結(jié)
在開發(fā)過程中現(xiàn)階段熱修復(fù)技術(shù)還是很火的,而一些大公司也相繼給出了一些熱修復(fù)的合理方案,每家都有各自的優(yōu)點和缺點,而我就要做到每家熱修復(fù)框架的詳細(xì)原理解析,從中能夠?qū)W習(xí)到更多的技巧和知識,頂著頭疼的風(fēng)險寫完了這篇文章,記得多點贊多分享!

更多內(nèi)容:點擊這里

關(guān)注微信公眾號,最新技術(shù)干貨實時推送



******掃一掃加小編微信****添加時注明:“編碼美麗”否則不予通過!**

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 媽媽,曾經(jīng)年輕美麗能干的媽媽,一下子蒼老了,而且患了早期老年癡呆癥。她不再風(fēng)風(fēng)火火忙東忙西,不再是家里的頂梁柱,不...
    我愛我de家閱讀 544評論 0 2
  • 文/橘子醬 那個嫌疑犯被審問的時候,我就坐在玻璃窗外細(xì)細(xì)查看著。審訊的警察a情緒有些激動:“她可是你的妻子,你為了...
    橘子醬__閱讀 385評論 1 3
  • 恩,沒錯,三周了,之前本來是想著每一周就寫一次心得體會來審核自己并總結(jié),好吧,各種原因,各種懶惰。然后,常見...
    DWade3閱讀 977評論 0 7

友情鏈接更多精彩內(nèi)容