深入理解Android熱修復技術(shù)原理之so庫熱修復技術(shù)
一、SO庫加載原理
Java Api 提供以下兩個接口加載一個 so 庫
- System. loadLibrary (String libName):傳進去的參數(shù):so庫名稱, 表示的so 庫文件,位于apk壓縮文件中的 libs 目錄,最后復制到 apk安裝目錄下。
- System, load (String pathName):傳進去的參數(shù): so庫在磁盤中的完整 路徑。加載一個自定義外部 so庫文件。
上述兩種方式加載一個 so 庫,實際上最后都調(diào)用 nativeLoad 這個 native方法去加載 so庫,這個方法的 fileName:so 庫在磁盤中的完整路徑名。
代碼+圖文的方式簡述 so 庫加載原理,下面的代碼示例,stringFromJNI -> Java_com_taobao_jni_MainActivity_stringFromJNI 靜態(tài)注冊的 native 方 法,test->test 動態(tài)注冊的 native 方法。
我們知道 JNI 編程中,動態(tài)注冊的 native 方法必須實現(xiàn) JNI_OnLoad方法,同時實現(xiàn)一個JNINativeMethod [] 數(shù)組,靜態(tài)注冊的 native 方法必須是Java+類完整路徑+方法名的格式。
總結(jié)下:
- 動態(tài)注冊的 native 方法映射通過加載 so 庫過程中調(diào)用 JNI_onLoad 方法調(diào)用完成。
- 靜態(tài)注冊的 native 方法映射是在該native方法第一次執(zhí)行的時候才完成映射,當然前提是該 so 庫已經(jīng) load 過。
二、SO庫熱部署實時生效可行性分析
2.1、動態(tài)注冊 native 方法實時生效
前面我們分析過 so 庫的加載原理,我們知道動態(tài)注冊的 native方法調(diào)用一次 JNI_OnLoad 方法都會重新完成一次映射,所以我們是否只要先加載原來的 so庫, 然后再加載補丁 so 庫,就能完成Java層 native 方法到 native 層 patch后的新方法映射,這樣就完成動態(tài)注冊native 方法的 patch 實時修復。一張圖說明
實測發(fā)現(xiàn) art 下這樣是可以做到實時生效的,但是 Dalvik下做不到實時生效,通 過代碼測試我們發(fā)現(xiàn),實際上Dalvik 下第二次 load補丁 so庫,執(zhí)行的仍然是原來so 庫的 JNI_0nLoad方法,而不是補丁so 庫的 JNI_OnLoad 方法,所以 Dalvik 下做不到實時生效。我們來簡單分析下,既然拿到的是原來 so 庫的 JNI_OnLoad方法,那么我們首先懷疑以下兩個函數(shù)是否有問題。
- dlopen() :返回給我們一個動態(tài)鏈接庫的句柄
- disym() :通過一個 dlopen 得到的動態(tài)連接庫句柄,來查找一個 symbol
首先來看下 Dalvik 虛擬機下面 dlopen 的實現(xiàn),源碼在 /bionic/linker/dlfcn.cpp 文件,方法調(diào)用鏈路:dlopen -> do_d.lopen -> find_library -> find_library_internal
findloadedlibrary 方法判斷 name 表示的 so庫是否已經(jīng)被加載過,如果加載過直接返回之前加載 so庫的句柄,沒有加載過,調(diào)用 load_library嘗試加載 so庫
看代碼注釋,也知道其實這是Dalvik虛擬機下的一個 bug,這里它是通過 basename 去做查找,傳進來的參數(shù) name 實際上是 so庫所在磁盤的完整路徑,比如此時修復后的so庫的路徑為 /data/data/com. taobao. jni/files/libnative-lib.so。但是此時是通過 bname : libnative-lib.so 作為 key 去查找, 我們知道第一次加載原來的 so庫 System.loadLibrary ( "native-lib");實際上已經(jīng)在solist表中存在了 native-lib 這個 key,所以 Dalvik下面加載修復后的補丁so拿到的還是原so庫文件的句柄,所以執(zhí)行的仍然是原來 so庫的JNI_ OnLoad方法,Art下不存在這個問題,是因為Art下這個地方是以name作為key 去查找而不是bname,所以art 重新load —遍補丁 so庫:拿到的是補丁 so庫的句柄,然后執(zhí)行補丁庫的JNI OnLoad。
所以為了解決 Dalvik 下面的這個問題,那么如果嘗試對補丁 so進行改名,比如 此處補丁so 庫的完整路徑修改之后變成 /data/data/com.taobao.jni/files/ libnative-lib-123333.so,后面一串數(shù)字是當前時間戳,確保這個 bname是全局唯一的,按照上面的分析,在solist 中查找的 key已經(jīng)是唯一的,所以此時可以做到Dalvik 下面動態(tài)注冊的 native 方法的實時生效。
2.2、靜態(tài)注冊 native 方法實時生效
上面通過嘗試對補丁 so庫進行重命名為全局唯一的名稱可以確保第二次加載補丁so 庫可以做到 Dalvik 下和 Art下動態(tài)注冊方法的實時生效,但要做到靜態(tài)注冊 native 方法的實時生效還需要更多工作。
前面我們說過靜態(tài)注冊 native 方法的映射是在 native方法第一次執(zhí)行的時候就完成了映射,所以如果native方法在加載補丁 so 庫之前已經(jīng)執(zhí)行過了,那么是否這種時候這個靜態(tài)注冊的 native 方法一定得不到修復?幸運的是,系統(tǒng) JNI API提供 了解注冊的接口。
UnregisterNatives 函數(shù)會把 jclazz 所在類的所有 native 方法都重新指向為 dvmResolveNativeMethod,所以調(diào)用 UnregisterNatives 之后不管是靜態(tài)注冊還是動態(tài)注冊的native方法之前是否執(zhí)行過在加載補丁 so的時候都會重新去做映射。所以我們只需要以下調(diào)用。
這里有一個難點,因為 native 方法的修改是在 so庫中,所以我們的補丁工具很難檢測出到底是哪個Java 類需要解注冊 native 方法。這個問題暫且放下。假設(shè)我們能知道哪個類需要解注冊native方法,然后 load補丁 so庫之后,再次執(zhí)行該 native 方法,這樣看起來是可以讓該 native方法實時生效,但是測試發(fā)現(xiàn),在補丁 so 庫重命名的前提下,java 層 native 方法可能映射到原so庫的方法,也可能映射到補丁 so 庫的修復后的新方法。
首先靜態(tài)注冊的 native方法之前從未執(zhí)行,首先嘗試解析該方法。或者調(diào)用了 unregisterJNINativeMethods 解注冊方法,那么該方法將指向 meth->nativeFunc = dvmResolveNativeMethod,那么真正運行該方法的時候,實際上執(zhí)行的是dvmResolveNativeMethod 函數(shù)。這個函數(shù)主要完成 java 層 native方法和native 層方法的映射邏輯。
gDvm.nativeLibs 是一個全局變量,它是一個hashtable,存放著整個虛擬機加載 so庫的 SharedLib 結(jié)構(gòu)指針。然后該變量作為參數(shù)傳遞給 dvmHashForeach 函數(shù)進行 hashtable 遍歷。執(zhí)行 findMethodInLib 函數(shù)看是否找到對應(yīng)的 native函 數(shù)指針,如果第一個找到就直接return,不在進行下次的查找。
這個結(jié)構(gòu)很重要,在虛擬機中大量使用到了 hashtable 這個數(shù)據(jù)結(jié)構(gòu),hashtable 的實現(xiàn)源碼在 dalvik/vm/Hash.h 和 dalvik/vm/Hash.cpp 文件中,有興趣可以自行查看源碼,這里不進行詳細分析。hashtable的遍歷和插入都是在 dvmHashTableLookup 方法中實現(xiàn),簡單說下 java.hashtable 和 c.hashtable 的異同點:
- 共同點:兩者實際上都是數(shù)組實現(xiàn),hashtable容量如果超過默認值都會進行擴容,都是對key進行hash計算然后跟hashtable的長度進行取模作為 bucket。
- 不同點:Dalvik 虛擬機下 hashtable put/get操作實現(xiàn)方法,實際上實現(xiàn)要 比java hashmap 的實現(xiàn)要簡單一些,java hashmap 的 put實現(xiàn)需要處理 hash沖突的情況,一般情況下會通過在沖突節(jié)點上新增一個鏈表處理沖突, 然后get實現(xiàn)會遍歷這個鏈表通過equals方法比較value是否一致進行查找,davlik 下 hashtable 的 put 實現(xiàn)上 (doAdd=true) 只是簡單的把指針 下移直到下一個空節(jié)點。get 實現(xiàn) (doAdd=false) 首先根據(jù) hash值計算出 bucket 位置,然后通過 cmpFunc函數(shù)比較值是否一致,不一致,指針下移。 hashtable 的遍歷實際就是數(shù)組遍歷實現(xiàn)
知道了 davlik 下 hashtable的實現(xiàn)原理,那我們再來看下前面提到的:補丁 so庫重命名的前提下,為什么 java 層 native 方法可能映射到原 so 庫的方法也可能映射到補丁 so庫的修復后的新方法。一張圖說明情況
所以我們可以得到結(jié)論:
對補丁 so庫進行重命名后,如果這個補丁 so庫在hashtable中的位置比原 so庫的位置靠前,那么這個靜態(tài)注冊native方法就能夠得到修復,位置如果靠后就得不到修復。
2.3、SO實時生效方案總結(jié)
基于上面的分析,so庫的實時生效必須滿足以下幾點:
- so庫為了兼容Dalvik虛擬機下動態(tài)注冊native方法的實時生效,必須對so 文件進行改名。
- 針對so庫靜態(tài)注冊native方法的實時生效,首先需要解注冊靜態(tài)注冊的 native方法,這個也是難點,因為我們很難知道so庫中哪幾個靜態(tài)注冊的 native方法發(fā)生了變更。假設(shè)就算我們知道如果靜態(tài)注冊的native方法需要解注冊,重新load補丁 so庫也有可能被修復也有可能不被修復。
- 上面對補丁 so進行了第二次加載,那么肯定是多消耗了一次本地內(nèi)存,如果 補丁 so庫夠大,補丁 so夠多,那么JNI層的OOM也不是沒可能
- 另外一方面補丁 so如果新增了一個動態(tài)注冊的方法而dex中沒有相應(yīng)方法, 直接去加載這個補丁 so文件會報NoSuchMethodError異常,具體邏輯在 dvmRegisterJNIMethod中。我們知道如果dex如果新增了—native 方法,那么走不了熱部署只能冷啟動重啟生效,所以此時補丁so就不能第二 次load 了。這種情況下so庫的修復嚴重依賴于dex的修復方案。
可以看到 so庫實時生效方案,對于靜態(tài)注冊的native方法有一定的局限性, 不能滿足一般的通用性,所以最后我們放棄了 so庫的實時生效需求,轉(zhuǎn)而求次實現(xiàn) so庫修復的冷部署重啟生效方案。
三、SO庫冷部署重啟生效實現(xiàn)方案
為了更好的兼容通用性,我們嘗試通過冷部署重啟生效的角度分析下補丁 so庫的修復方案。
3.1、接口調(diào)用替換方案
sdk提供接口替換System默認加載so庫接口
SOPatchManager.loadLibrary(String libName) -> System.loadLibrary(String libName)
SOPatchManager.loadLibrary接口加載 so庫的時候優(yōu)先嘗試去加載sdk 指定目錄下的補丁so,加載策略如下:
如果存在則加載補丁 so庫而不會去加載安裝apk安裝目錄下的so庫
如果不存在補丁so,那么調(diào)用System.loadLibrary去加載安裝apk目錄下的 so庫。
我們可以很清楚的看到這個方案的優(yōu)缺點:
- 優(yōu)點:不需要對不同 sdk 版本進行兼容,因為所有的 sdk 版本都有 System.loadLibrary 這個接口。
- 缺點:調(diào)用方需要替換掉 System 默認加載 so 庫接口為 sdk提供的接口, 如果是已經(jīng)編譯混淆好的三方庫的so 庫需要 patch,那么是很難做到接口的替換。
雖然這種方案實現(xiàn)簡單,同時不需要對不同 sdk版本區(qū)分處理,但是有一定的局限性沒法修復三方包的so庫同時需要強制侵入接入方接口調(diào)用,接著我們來看下反射注入方案。
3.2、反射注入方案
前面介紹過 System. loadLibrary ( "native-lib"); 加載 so庫的原理,其實native-lib 這個 so 庫最終傳給 native 方法執(zhí)行的參數(shù)是 so庫在磁盤中的完整路徑,比如:/data/app-lib/com.taobao.jni-2/libnative-lib.so, so庫會在 DexPathList.nativeLibraryDirectories/nativeLibraryPathElements 變量所表示的目錄下去遍歷搜索。
sdk<23 DexPathList.findLibrary 實現(xiàn)如下
可以發(fā)現(xiàn)會遍歷 nativeLibraryDirectories數(shù)組,如果找到了 loUtils.canOpenReadOnly (path)返回為 true, 那么就直接返回該 path, loUtils.canOpenReadOnly (path)返回為 true 的前提肯定是需要 path 表示的 so文件存 在的。那么我們可以采取類似類修復反射注入方式,只要把我們的補丁so庫的路徑插入到nativeLibraryDirectories數(shù)組的最前面就能夠達到加載so庫的時候是補丁 庫而不是原來so庫的目錄,從而達到修復的目的。
sdk>=23 DexPathList.findLibrary 實現(xiàn)如下
sdk23 以上 findLibrary 實現(xiàn)已經(jīng)發(fā)生了變化,如上所示,那么我們只需要把補丁so庫的完整路徑作為參數(shù)構(gòu)建一個Element對象,然后再插入到nativeLibraryPathElements 數(shù)組的最前面就好了。
- 優(yōu)點:可以修復三方庫的so庫。同時接入方不需要像方案1 —樣強制侵入用 戶接口調(diào)用
- 缺點:需要不斷的對 sdk 進行適配,如上 sdk23 為分界線,findLibrary接口實現(xiàn)已經(jīng)發(fā)生了變化。
我們知道在不管是在補丁包中還是 apk 中一個 so 庫都存在多種 cpu 架構(gòu)的 so 文件,比如"armeabi","arm64-v8a","x86"等。加載肯定是加載其中一個 so庫文件的,如何選擇機型對應(yīng)的 so 庫文件將是重點所在。
四、如何正確復制補丁 SO庫
上面提到的一個問題,這里不打算詳細介紹。有需要的參考文檔:Android動態(tài) 鏈接庫加載原理及HotFix方案介紹,這篇文檔有些觀點不盡正確,但是我也能知道虛擬機究竟選擇哪個abis目錄作為參數(shù)構(gòu)建PathClassLoader對象,一張圖簡單了解下原理:
實際上補丁 so也存在類似的問題,我們的補丁 so庫文件放到補丁包的libs目錄下面,libs目錄和.dex文件和res資源文件一起打包成一個壓縮文件作為最后的補丁包,libs目錄可能也包含多種abis目錄。所以我們需要選擇手機最合適的 primaryCpuAbi,然后從libs目錄下面選擇這個primaryCpuAbi子目錄插入到 nativeLibraryDirectories/nativeLibraryPathElements 數(shù)組中。所以怎么選擇primaryCpuAbi是關(guān)鍵,來看下我們sdk具體的實現(xiàn)
- sdk>=21 時,直接反射拿到 Applicationinfo 對象的 primaryCpuAbi 即可
- sdk<21 時,由于此時不支持 64 位,所以直接把Build.CPU_ABI, Build.CPU_ABI2 作為 primaryCpuAbi 即可
五、本章小結(jié)
對于 so庫的修復方案目前更多采取的是接口調(diào)用替換方式,需要強制侵入用戶 接口調(diào)用。目前我們的so文件修復方案采取的是反射注入的方案,重啟生效。具有更好的普遍性。如果有so文件修復實時生效的需求,也是可以做到的,只是有些限制情況。
以上就是深入理解Android熱修復技術(shù)原理之so庫熱修復技術(shù)的詳細內(nèi)容,更多關(guān)于Android so庫熱修復的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義控件ViewFipper實現(xiàn)豎直跑馬燈效果
這篇文章主要為大家詳細介紹了Android自定義控件ViewFipper實現(xiàn)豎直跑馬燈效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12Android開發(fā)實現(xiàn)的圓角按鈕、文字陰影按鈕效果示例
這篇文章主要介紹了Android開發(fā)實現(xiàn)的圓角按鈕、文字陰影按鈕效果,涉及Android界面布局與屬性設(shè)置相關(guān)操作技巧,需要的朋友可以參考下2019-04-04Android應(yīng)用開發(fā)中RecyclerView組件使用入門教程
這篇文章主要介紹了Android應(yīng)用開發(fā)中RecyclerView組件使用的入門教程,RecyclerView主要針對安卓5.0以上的material design開發(fā)提供支持,需要的朋友可以參考下2016-02-02AndroidStudio中重載方法@Override的使用詳解
這篇文章主要介紹了AndroidStudio中重載方法@Override的使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04Android使用MediaRecorder類實現(xiàn)視頻和音頻錄制功能
Android提供了MediaRecorder這一個類來實現(xiàn)視頻和音頻的錄制功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友參考下吧2018-07-07Android使用CardView作為RecyclerView的Item并實現(xiàn)拖拽和左滑刪除
這篇文章主要介紹了Android使用CardView作為RecyclerView的Item并實現(xiàn)拖拽和左滑刪除,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11