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