前言
Android外掛程式化工程具有減少方法數和包大小, 易於擴展等優勢, 深得大型工程的青睞, 但同時外掛程式化也會引起一些意想不到的麻煩。 我們最近在做的外掛程式工程就遇到了一個詭異的JNI連結錯誤。
我們的外掛程式工程作為主工程的具體業務, 主工程提供了基礎的類庫和工具, 外掛程式工程有自己的ClassLoader, 並把主工程的ClassLoader設為自己的父ClassLoader, 通過雙親委託, 外掛程式工程就可以訪問主工程中的類。 在主工程中有一個類庫, 有JNI方法, 但為了減少主工程的包大小, so檔由外掛程式在用到時自己下載和載入。
而這種載入方式, 出現了詭異的UnsatisfiedLinkError錯誤。 我們首先檢查了System.load方法發現並沒有出錯, 又查看了進程的記憶體映射資訊, 發現so檔確實已經載入, 但調用JNI方法也確實一直出錯。 待排查了時序等相關情況後, 還是不成功, 於是我們只得求助於系統源碼, 希望能從源碼中找到答案, 以Android N為例, 我們開始了源碼分析過程。
so載入流程分析
so既然要先載入才能用, 那我們就先來看so是怎麼載入的, 先來分析System.load方法
方法很簡單, 直接調用了Runtime類的load方法, 傳入了so的名稱和當前的ClassLoader, 再來看這個方法
可以看到, load校驗了參數後調用了doLoad方法, doLoad取得ldLibraryPath和dexPath後調用了native層的nativeLoad函數。 繼續看nativeLoad函數
還是很簡單的函數, 設置完LdLibraryPath後, 調用JavaVM的LoadNtiveLibrary函數, 繼續看
該函數較長, 但邏輯還是很清晰的, 我們只列出了關鍵代碼, libraries保存了一個以so路徑和SharedLibrary物件為記錄的Map, 保存了當前所有已經載入的so。 首先從libraries中查找記錄, 如果有說明該so已經載入過,
native方法執行流程分析
我們知道, 在ART環境下, 類的方法都會用ArtMethod表示, 而ArtMethod的PtrSizedFields欄位保存了該方法的跳轉位址
其中entry_point_fromjni就是native函數執行時的跳轉位址, 那麼這個位址是什麼呢?其實這個位址是Class在載入的時候設置的, 我們來看下代碼
ClassLinker負責在ART中載入Class,通過FindClass->DefineClass->LoadClass->LoadClassMembers,會解析出ArtMethod,最後通過LinkCode對ArtMethod的跳轉位址進行賦值,這裡我們只看native方法的情況,執行了UnregisterNative函數
SetEntryPointFromJni就是對entry_point_fromjni做了賦值,值是通過GetJniDlsymLookupStub()獲得,就是一個artjnidlsymlookupstub函數位址,到這裡我們知道類載入後其native方法位址被設置成了artjnidlsymlookupstub這個入口函數,當native方法被執行時,會調用這個入口函數執行,我們來看這個函數
art_jni_dlsym_lookup_stub在彙編中定義,與平臺相關,我們用arm64平臺代碼作為例子
可以看到這個函數又跳轉到了artFindNativeMethod函數
該函數首先查詢native函數的位址,查到後會通過RegisterNative設置給ArtMethod,這樣以後就ArtMethod就可以直接跳轉到native層的位址,而不用每次都經過該函數,RegisterNative同樣調用了SetEntryPointFromJni來設置跳轉位址,接下來看FindCodeForNativeMethod函數
這裡又看到了熟悉的libraries,前邊分析so載入部分已經知道它保存了所有已經載入的so,所以這就是從已經載入的so裡查找native函數,如果沒找到,則拋出UnsatisfiedLinkError。我們再來看看FindNativeMethod
FindSymbol就是調用dlsym獲取native函數的位址,所以到此native函數的位址就真正的找到了,但是我們注意到了其中的一個判斷,library->GetClassLoader()==declaring_class_loader,也就是和so關聯的ClassLoader要和當前的ClassLoader是同一個才行,不然會放棄查找,到此我們的疑惑也就解開了,因為JAVA層的代碼是在主工程的ClassLoader裡,而載入so用的是外掛程式的ClassLoader,兩個ClassLoader不相等,所以在這裡放棄了查找而拋出了異常。
解決方案
知道了原因解決自然也就容易了,只要用同一個ClassLoader載入類和so就行了,因為Java層的ClassLoader是變不了的,所以我們就改變載入so的ClassLoader
1、使用主工程中的類來載入so
2、如果主工程不好添加代碼的話,我們也可以在外掛程式裡改變Runtime.load()所使用的ClassLoader,但是Runtime的load方法只有一個參數的公開方法,傳ClassLoader的方法是私有的,所以我們只能通過反射去傳入主工程的ClassLoader
一點思考
通常我們只注意了Java類和ClassLoader的對應關係,JVM通過ClassLoader和類的全路徑名來唯一的確定一個class,而忽略了so和ClassLoader也是有對應關係的,具有相同ClassLoader的Java類和JNI方法才能一一對應,ClassLoader其實也起到了類似命名空間的作用。
ClassLinker負責在ART中載入Class,通過FindClass->DefineClass->LoadClass->LoadClassMembers,會解析出ArtMethod,最後通過LinkCode對ArtMethod的跳轉位址進行賦值,這裡我們只看native方法的情況,執行了UnregisterNative函數
SetEntryPointFromJni就是對entry_point_fromjni做了賦值,值是通過GetJniDlsymLookupStub()獲得,就是一個artjnidlsymlookupstub函數位址,到這裡我們知道類載入後其native方法位址被設置成了artjnidlsymlookupstub這個入口函數,當native方法被執行時,會調用這個入口函數執行,我們來看這個函數
art_jni_dlsym_lookup_stub在彙編中定義,與平臺相關,我們用arm64平臺代碼作為例子
可以看到這個函數又跳轉到了artFindNativeMethod函數
該函數首先查詢native函數的位址,查到後會通過RegisterNative設置給ArtMethod,這樣以後就ArtMethod就可以直接跳轉到native層的位址,而不用每次都經過該函數,RegisterNative同樣調用了SetEntryPointFromJni來設置跳轉位址,接下來看FindCodeForNativeMethod函數
這裡又看到了熟悉的libraries,前邊分析so載入部分已經知道它保存了所有已經載入的so,所以這就是從已經載入的so裡查找native函數,如果沒找到,則拋出UnsatisfiedLinkError。我們再來看看FindNativeMethod
FindSymbol就是調用dlsym獲取native函數的位址,所以到此native函數的位址就真正的找到了,但是我們注意到了其中的一個判斷,library->GetClassLoader()==declaring_class_loader,也就是和so關聯的ClassLoader要和當前的ClassLoader是同一個才行,不然會放棄查找,到此我們的疑惑也就解開了,因為JAVA層的代碼是在主工程的ClassLoader裡,而載入so用的是外掛程式的ClassLoader,兩個ClassLoader不相等,所以在這裡放棄了查找而拋出了異常。
解決方案
知道了原因解決自然也就容易了,只要用同一個ClassLoader載入類和so就行了,因為Java層的ClassLoader是變不了的,所以我們就改變載入so的ClassLoader
1、使用主工程中的類來載入so
2、如果主工程不好添加代碼的話,我們也可以在外掛程式裡改變Runtime.load()所使用的ClassLoader,但是Runtime的load方法只有一個參數的公開方法,傳ClassLoader的方法是私有的,所以我們只能通過反射去傳入主工程的ClassLoader
一點思考
通常我們只注意了Java類和ClassLoader的對應關係,JVM通過ClassLoader和類的全路徑名來唯一的確定一個class,而忽略了so和ClassLoader也是有對應關係的,具有相同ClassLoader的Java類和JNI方法才能一一對應,ClassLoader其實也起到了類似命名空間的作用。