您的位置:首頁>正文

一個 ClassLoader 引起的 JNI 連結錯誤

前言

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已經載入過,

再判斷和so關聯的ClassLoader是不是當前的ClassLoader, 如果不是, 返回false, 這說明同一個路徑的so只能被一個ClassLoader載入, 如果沒找到記錄, 說明該so沒有載入過, 則通過dlopen打開該so, 保存相關資訊到SharedLibrary物件中, 把SharedLibrary添加到libraries中, 用dlsym查找JNI_OnLoad函數, 如果找到了則執行該函數。 在看代碼時第一反應是會不會isSameObject判斷這裡有問題, so已經被另一個ClassLoader給載入了, 但轉念一想, 如果這裡有問題那麼load的時候會直接報錯, 而不是在執行的時候才報錯。 所以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其實也起到了類似命名空間的作用。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示