目錄介紹
- 01.學(xué)習(xí)JNI開發(fā)流程
- 1.1 JNI開發(fā)概念
- 1.2 JNI和NDK的關(guān)系
- 1.3 JNI實踐步驟
- 1.4 NDK使用場景
- 1.5 學(xué)習(xí)路線說明
- 02.NDK架構(gòu)分層
- 2.1 NDK分層構(gòu)建層
- 2.2 NDK分層Java層
- 2.3 Native層
- 03.JNI基礎(chǔ)語法
- 3.1 JNI三種引用
- 3.2 JNI異常處理
- 3.3 C和C++互相調(diào)用
- 3.4 JNI核心原理
- 3.5 注冊Native函數(shù)
- 3.6 JNI簽名是什么
- 04.一些必備操作
- 4.1 so庫生成打包
- 4.2 so庫查詢操作
- 4.3 so庫如何反編譯
- 05.實踐幾個案例
- 5.1 Java靜態(tài)調(diào)用C/C++
- 5.2 C/C++調(diào)用Java
- 5.3 Java調(diào)三方so中API
- 5.4 Java動態(tài)調(diào)C++
- 06.一些技術(shù)原理
- 6.1 JNIEnv創(chuàng)建和釋放
- 6.2 動態(tài)注冊的原理
- 6.3 注冊JNI流程圖
- 07.JNI遇到的問題
- 7.1 混淆的bug
- 7.2 注意字符串編譯
01.學(xué)習(xí)JNI開發(fā)流程
1.1 JNI開發(fā)概念
- .SO庫是什么東西
- NDK為了方便使用,提供了一些腳本,使得更容易的編譯C/C<ins>代碼。在Android程序編譯中會將C/C</ins> 編譯成動態(tài)庫 so 文件,類似java庫.jar文件一樣,它的生成需要使用NDK工具來打包。
- so是shared object的縮寫,見名思義就是共享的對象,機器可以直接運行的二進制代碼。實質(zhì)so文件就是一堆C、C++的頭文件和實現(xiàn)文件打包成一個庫。
- JNI是什么東西
- JNI的全稱是Java Native Interface,即本地Java接口。因為 Java 具備跨平臺的特點,所以Java 與 本地代碼交互的能力非常弱。
- 采用JNI特性可以增強 Java 與本地代碼交互的能力,使Java和其他類型的語言如C++/C能夠互相調(diào)用。
1.2 JNI和NDK的關(guān)系
- JNI和NDK學(xué)習(xí)內(nèi)容太難
- 其實難的不是JNI和NDK,而是C/C++語言,JNI和NDK只是個工具,很容易學(xué)習(xí)的。
- JNI和NDK有何聯(lián)系
- 學(xué)習(xí)JNI之前,首先得先知道JNI、NDK、Java和C/C++之間的關(guān)系。
- 在Android開發(fā)中,有時為了性能和安全性(反編譯),需要使用C/C++語言,但是Android APP層用的是Java語言,怎么才能讓這兩種語言進行交流呢,因為他們的編碼方式是不一樣的,這是就需要JNI了。
- JNI可以被看作是代理模式,JNI是java接口,用于Java與C/C<ins>之間的交互,作為兩者的橋梁,也就是Java讓JNI代其與C/C</ins>溝通。
- NDK是Android工具開發(fā)包,幫助快速開發(fā)C/C++動態(tài)庫,相當(dāng)于JDK開發(fā)java程序一樣,同時能幫打包生成.so庫
1.3 JNI實踐步驟
- 操作實踐步驟
- 第一步,編寫native方法。
- 第二步,根據(jù)此native方法編寫C文件。
- 第三步,使用NDK打包成.so庫。
- 第四步,使用.so庫然后調(diào)用api。
- 如何使用NDK打包.so庫
- 1,編寫Android.mk文件,此文件用來告知NDK打包.so庫的規(guī)則
- 2,使用ndk-build打包.so庫
- 相關(guān)學(xué)習(xí)文檔
- NDK學(xué)習(xí):https://developer.android.google.cn/ndk/guides?hl=zh-cn
1.4 NDK使用場景
- NDK的使用場景一般在:
- 1.為了提升這些模塊的性能,對圖形,視頻,音頻等計算密集型應(yīng)用,將復(fù)雜模塊計算封裝在.so或者.a文件中處理。
- 2.使用的是C/C++進行編寫的第三方庫移植。如ffmppeg,OpenGl等。
- 3.某些情況下為了提高數(shù)據(jù)安全性,也會封裝so來實現(xiàn)。畢竟使用純Java開發(fā)的app是有很多逆向工具可以破解的。
1.5 學(xué)習(xí)路線說明
- JNI學(xué)習(xí)路線介紹
- 1.首先要有點C/C++的基礎(chǔ),這個我是在 菜鳥教程 上學(xué)習(xí)的
- 2.理解NDK和JNI的一些概念,以及NDK的一個大概的架構(gòu)分層,JNI的開發(fā)步驟是怎樣的
- 3.掌握案例練習(xí),前期先寫案例,比如java調(diào)用c/c++,或者c/c++調(diào)用java。把這個案例寫熟,跑通即可
- 4.案例練習(xí)之后,然后在思考NDK是怎么編譯的,如何打包so文件,loadLibrary的流程,CMake工作流程等一些基礎(chǔ)的原理
- 5.在實踐過程中,先記錄遇到的問題。這時候可能不一定懂,先放著,先實現(xiàn)案例或者簡單的業(yè)務(wù)。然后邊實踐邊琢磨問題和背后的原理
- 注意事項介紹
- 避免一開始就研究原理,或者把C/C++整體學(xué)習(xí)一遍,那樣會比較辛苦。焦點先放在JNI通信流程上,寫案例學(xué)習(xí)
- 把學(xué)習(xí)內(nèi)容,分為幾個不同類型:了解(能夠扯淡),理解(大概知道什么意思),掌握(能夠運用和實踐),精通(能舉一反三和分享講清楚)
02.NDK架構(gòu)分層
- 使用NDK開發(fā)最終目標是為了將C/C++代碼編譯生成.so動態(tài)庫或者靜態(tài)庫文件,并將庫文件提供給Java代碼調(diào)用。
- 所以按架構(gòu)來分可以分為以下三層:
- 1.構(gòu)建層
- 2.Java層
- 3.native層
2.1 NDK分層構(gòu)建層
- 要得到目標的so文件,需要有個構(gòu)建環(huán)境以及過程,將這個過程和環(huán)境稱為構(gòu)建層。
- 構(gòu)建層需要將C/C++代碼編譯為動態(tài)庫so,那么這個編譯的過程就需要一個構(gòu)建工具,構(gòu)建工具按照開發(fā)者指定的規(guī)則方式來構(gòu)建庫文件,類似apk的Gradle構(gòu)建過程。
- 在講解NDK構(gòu)建工具之前,我們先來了解一些關(guān)于CPU架構(gòu)的知識點:Android abi
- ABI即Application Binary Interface,定義了二進制接口交互規(guī)則,以適應(yīng)不同的CPU,一個ABI對應(yīng)一種類型的CPU。
- Android目前支持以下7種ABI:
- 1.armeabi:第5代和6代的ARM處理器,早期手機用的比較多。
- 2.armeabi-v7a:第7代及以上的 ARM 處理器。
- 3.arm64-v8a:第8代,64位ARM處理器
- 4.x86:一般用在平板,模擬器。
- 5.x86_64:64位平板。
- 常規(guī)的NDK構(gòu)建工具有兩種:
- 1.ndk-build:
- 2.Cmake
- ndk-build其實就是一個腳本。早期的NDK開發(fā)一直都是使用這種模式
- 運行ndk-build相當(dāng)于運行一下命令:$GNUMAKE -f <ndk>/build/core/build-local.mk
- $GNUMAKE 指向 GNU Make 3.81 或更高版本,<ndk> 則指向 NDK 安裝目錄
- 使用ndk-build需要配合兩個mk文件:Android.mk和Application.mk。
- Cmake是一個編譯系統(tǒng)的生成器
- 簡單理解就是,他是用來生成makefile文件的,Android.mk其實就是一個makefile類文件,cmake使用一個CmakeLists.txt的配置文件來生成對應(yīng)的makefile文件。
- Cmake構(gòu)建so的過程其實包括兩步:步驟1:使用Cmake生成編譯的makefiles文件;步驟2:使用Make工具對步驟1中的makefiles文件進行編譯為庫或者可執(zhí)行文件。
- Cmake優(yōu)勢在哪里呢?在生成makefile過程中會自動分析源代碼,創(chuàng)建一個組件之間依賴的關(guān)系樹,這樣就可以大大縮減在make編譯階段的時間。
- Cmake構(gòu)建項目配置
- 使用Cmake進行構(gòu)建需要在build.gradle配置文件中聲明externalNativeBuild
2.2 NDK分層Java層
-
如何選擇正確的so庫呢
- 通常情況下,我們在編譯so的時候就需要確定自己設(shè)備類型,根據(jù)設(shè)備類型選擇對應(yīng)abiFilters。
- 注意:使用as編譯后的so會自動打包到apk中,如果需要提供給第三方使用,可以到build/intermediates/cmake/debug or release 目錄中copy出來。
-
Java層如何調(diào)用so文件中的函數(shù)
- 對于Android上層代碼來說,在將包正確導(dǎo)入到項目中后,只需要一行代碼就可以完成動態(tài)庫的加載過程。有兩種方式:
System.load("/data/local/tmp/native_lib.so"); System.loadLibrary("native_lib");- 1.加載路徑不同:load是加載so的完整路徑,而loadLibrary是加載so的名稱,然后加上前綴lib和后綴.so去默認目錄下查找。
- 2.自動加載庫的依賴庫的不同:load不會自動加載依賴庫;而loadLibrary會自動加載依賴庫。
-
無論哪種方式,最終都會調(diào)用到LoadNativeLibrary()方法,該方法主要操作:
- 1.通過dlopen打開動態(tài)庫文件
- 2.通過dlsym找到JNI_OnLoad符號所對應(yīng)的方法地址
- 3.通過JNI_OnLoad去注冊對應(yīng)的jni方法
2.3 Native層
- 如何理解JNI的設(shè)計思想
- JNI(全名Java Native Interface)Java native接口,其可以讓一個運行在Java虛擬機中的Java代碼被調(diào)用或者調(diào)用native層的用C/C++編寫的基于本機硬件和操作系統(tǒng)的程序。簡單理解為就是一個連接Java層和Native層的橋梁。
- 開發(fā)者可以在native層通過JNI調(diào)用到Java層的代碼,也可以在Java層聲明native方法的調(diào)用入口。
- JNI注冊方式
- 當(dāng)Java代碼中執(zhí)行Native的代碼的時候,首先是通過一定的方法來找到這些native方法。JNI有靜態(tài)注冊和動態(tài)注冊兩種注冊方式。
- 靜態(tài)注冊先由Java得到本地方法的聲明,然后再通過JNI實現(xiàn)該聲明方法。動態(tài)注冊先通過JNI重載JNI_OnLoad()實現(xiàn)本地方法,然后直接在Java中調(diào)用本地方法。
03.JNI基礎(chǔ)語法
3.1 JNI三種引用
- 在JNI規(guī)范中定義了三種引用:
- 局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
- Local引用
- JNI中使用 jobject, jclass, and jstring等來標志一個Java對象,然而在JNI方法在使用的過程中會創(chuàng)建很多引用類型,如果使用過程中不注意就會導(dǎo)致內(nèi)存泄露。
- 直接使用:NewLocalRef來創(chuàng)建。Local引用其實就是Java中的局部引用,在聲明這個局部變量的方法結(jié)束或者退出其作用域后就會被GC回收。
- Global引用全局引用
- 全局引用可以跨方法、跨線程使用,直到被開發(fā)者顯式釋放。一個全局引用在被釋放前保證引用對象不被GC回收。
- 和局部應(yīng)用不同的是,能創(chuàng)建全局引用的函數(shù)只有NewGlobalRef,而釋放它需要使用ReleaseGlobalRef函數(shù)。
- Weak引用
- 弱引用可以使用全局聲明的方式。弱引用在內(nèi)存不足或者緊張的時候會自動回收掉,可能會出現(xiàn)短暫的內(nèi)存泄露,但是不會出現(xiàn)內(nèi)存溢出的情況。
3.2 JNI異常處理
- native層異常
- 處理方式1:native層自行處理
- 處理方式2:native層拋出給Java層處理
3.4 JNI核心原理
-
java運行在jvm,jvm本身就是使用C/C<ins>編寫的,因此jni只需要在java代碼、jvm、C/C</ins>代碼之間做切換即可
- [圖片上傳失敗...(image-3ae615-1687914888789)]
-
JNIEnv是什么?
- JINEnv是當(dāng)前Java線程的執(zhí)行環(huán)境,一個JVM對應(yīng)一個JavaVM結(jié)構(gòu)體,一個JVM中可能創(chuàng)建多個Java線程,每個線程對應(yīng)一個JNIEnv結(jié)構(gòu),它們保存在線程本地存儲TLS中。
- 因此不同的線程JNIEnv不同,而不能相互共享使用。 JavaEnv結(jié)構(gòu)也是一個函數(shù)表,在本地代碼通過JNIEnv函數(shù)表來操作Java數(shù)據(jù)或者調(diào)用Java方法。
3.5 注冊Native函數(shù)
- JNI靜態(tài)注冊:
- 步驟1.在Java中聲明native方法,比如:public native String stringFromJNI()
- 步驟2.在native層新建一個C/C++文件,并創(chuàng)建對應(yīng)的方法(建議使用AS快捷鍵自動生成函數(shù)名),比如:testjnilib.cpp: Line 8
- JNI動態(tài)注冊
- 通過RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而無需遵循特定的方法命名格式,這樣書寫起來會省事很多。
- 動態(tài)注冊其實就是使用到了前面分析的so加載原理:在最后一步的JNI_OnLoad中注冊對應(yīng)的jni方法。這樣在類加載的過程中就可以自動注冊native函數(shù)。比如:
- 與JNI_OnLoad()函數(shù)相對應(yīng)的有JNI_OnUnload()函數(shù),當(dāng)虛擬機釋放該C庫的時候,則會調(diào)用JNI_OnUnload()函數(shù)來進行善后清除工作。
- 那么如何選擇使用靜態(tài)注冊or動態(tài)注冊
- 動態(tài)注冊和靜態(tài)注冊最終都可以將native方法注冊到虛擬機中,推薦使用動態(tài)注冊,更不容易寫錯,靜態(tài)注冊每次增加一個新的方法都需要查看原函數(shù)類的包名。
3.6 JNI簽名是什么
- 為什么JNI中突然多出了一個概念叫”簽名”:
- 因為Java是支持函數(shù)重載的,也就是說,可以定義相同方法名,但是不同參數(shù)的方法,然后Java根據(jù)其不同的參數(shù),找到其對應(yīng)的實現(xiàn)的方法。
- 這樣是很好,所以說JNI肯定要支持的,如果僅僅是根據(jù)函數(shù)名,沒有辦法找到重載的函數(shù)的,所以為了解決這個問題,JNI就衍生了一個概念——”簽名”,即將參數(shù)類型和返回值類型的組合。
- 如果擁有一個該函數(shù)的簽名信息和這個函數(shù)的函數(shù)名,就可以順序的找到對應(yīng)的Java層中的函數(shù)。
- 如何查看簽名呢:可以使用javap命令。
- javap -s -p MainActivity.class
04.一些必備操作
4.1 so庫生成打包
- 什么是so文件庫
- so庫,即將C或者C++實現(xiàn)的功能進行打包,將其打包為共享庫,讓其他程序進行調(diào)用,這可以提高代碼的復(fù)用性。
- 關(guān)于.so文件的生成有兩種方式
- 可以提供給大家參考,一種是CMake自動生成法,另一種是傳統(tǒng)打包法。
- so文件在程序運行時就會加載
- 所以想使用Java調(diào)用.so文件,必有某個Java類運行時load了native庫,并通過JNI調(diào)用了它的方法。
- cmake生成.so方案
- 第一步:創(chuàng)建native C++ Project項目,創(chuàng)建native函數(shù)并實現(xiàn),先測試本地JNI函數(shù)調(diào)通
- 第二步:獲取.so文件。將生成的.apk文件改為.zip文件,然后進行解壓縮,就能看到.so文件。如果想支持多種庫架構(gòu),則可在module的build.gradle中配置ndk支持。
- 第三步:so文件測試。新建一個普通的Android程序,將so庫放入程序,然后創(chuàng)建類(注意要相同的包名、文件名及方法名)去加載so庫。
- 總結(jié)一下:Android Studio自動創(chuàng)建的native C++項目默認支持CMake方式,它支持JNI函數(shù)調(diào)用的入口在build.gradle中。
- 傳統(tǒng)打包生成.so方案【不推薦這種方式】
- 第一步:在Java類中聲明一個本地方法。
- 第二步:執(zhí)行指令javah獲得C聲明的.h文件。
- 第三步:獲得.c文件并實現(xiàn)本地方法。創(chuàng)建Android.mk和Application.mk,并配置其參數(shù),兩個文件如不編寫或編寫正常會出現(xiàn)報錯。
- 第四步:打包.so庫。cd到\app目錄下,執(zhí)行命令 ndk-build即可。生成so庫后,最后測試ok即可。
4.2 so庫查詢操作
-
so庫如何查找所對應(yīng)的位置
- 第一步:在 app 模塊的 build.gradle 中,追加以下代碼:
- 第二步:執(zhí)行命令行:./gradlew assembleDebug 【注意如果遇到gradlew找不到,則輸入:chmod +x gradlew】
-
so文件查詢結(jié)果后。就可以查詢到so文件屬于那個lib庫的!如下所示:libtestjnilib.so文件屬于TestJniLib庫的
find so file: /Users/yc/github/YCJniHelper/TestJniLib/build/intermediates/library_jni/debug/jni/armeabi-v7a/libtestjnilib.so find so file: /Users/yc/github/YCJniHelper/SafetyJniLib/build/intermediates/library_jni/debug/jni/armeabi-v7a/libsafetyjnilib.so find so file: /Users/yc/github/YCJniHelper/SignalHooker/build/intermediates/library_jni/debug/jni/armeabi-v7a/libsignal-hooker.so
05.實踐幾個案例
5.1 Java靜態(tài)調(diào)用C/C++
-
Java調(diào)用C/C++函數(shù)調(diào)用流程
- Java層調(diào)用某個函數(shù)時,會從對應(yīng)的JNI層中尋找該函數(shù)。根據(jù)java函數(shù)的包名、方法名、參數(shù)列表等多方面來確定函數(shù)是否存在。
- 如果沒有就會報錯,如果存在就會就會建立一個關(guān)聯(lián)關(guān)系,以后再調(diào)用時會直接使用這個函數(shù),這部分的操作由虛擬機完成。
-
Java層調(diào)用C/C++方法操作步驟
- 第一步:創(chuàng)建java類NativeLib,然后定義native方法stringFromJNI()
public native String stringFromJNI();- 第二步:根據(jù)此native方法編寫C文件,可以通過命令后或者studio提示生成C++對應(yīng)的方法函數(shù)
//java中stringFromJNI //extern “C” 指定以"C"的方式來實現(xiàn)native函數(shù) extern "C" //JNIEXPORT 宏定義,用于指定該函數(shù)是JNI函數(shù)。表示此函數(shù)可以被外部調(diào)用,在Android開發(fā)中不可省略 JNIEXPORT jstring //JNICALL 宏定義,用于指定該函數(shù)是JNI函數(shù)。,無實際意義,但是不可省略 JNICALL //以注意到j(luò)ni的取名規(guī)則,一般都是包名 + 類名,jni方法只是在前面加上了Java_,并把包名和類名之間的.換成了_ Java_com_yc_testjnilib_NativeLib_stringFromJNI(JNIEnv *env, jobject /* this */) { //JNIEnv 代表了JNI的環(huán)境,只要在本地代碼中拿到了JNIEnv和jobject //JNI層實現(xiàn)的方法都是通過JNIEnv 指針調(diào)用JNI層的方法訪問Java虛擬機,進而操作Java對象,這樣就能調(diào)用Java代碼。 //jobject thiz //在AS中自動為我們生成的JNI方法聲明都會帶一個這樣的參數(shù),這個instance就代表Java中native方法聲明所在的 std::string hello = "Hello from C++"; //思考一下,為什么直接返回字符串會出現(xiàn)錯誤提示? //return "hello"; return env->NewStringUTF(hello.c_str()); } -
舉一個例子
- 例如在 NativeLib 類的native stringFromJNI()方法,程序會自動在JNI層查找 Java_com_yc_testjnilib_NativeLib_stringFromJNI 函數(shù)接口,如未找到則報錯。如找到,則會調(diào)用native庫中的對應(yīng)函數(shù)。
5.2 C/C++調(diào)用Java
- Native層調(diào)用Java層的類的字段和方法的操作步驟
- 第一步:創(chuàng)建一個Native C++的Android項目,創(chuàng)建 Native Lib 項目
- 第二步:在cpp文件夾下創(chuàng)建:calljnilib.cpp文件,calljnilib.h文件(用來聲明calljnilib.cpp中的方法)。
- 第三步:開始編寫配置文件CmkaeLists.txt文件。使用add_library創(chuàng)建一個新的so庫
- 第四步:編寫 calljnilib.cpp文件。因為要實現(xiàn)native層調(diào)用Java層字段和方法,所以這里定義了兩個方法:callJavaField和callJavaMethod
- 第五步:編寫Java層的調(diào)用代碼此處要注意的是調(diào)用的類的類名以及包名都要和c++文件中聲明的一致,否則會報錯。具體看:CallNativeLib
- 第六步:調(diào)用代碼進行測試。然后查看測試結(jié)果
5.3 Java調(diào)三方so中API
- 直接拿前面案例的 calljnilib.so 來測試,但是為了實現(xiàn)三方調(diào)用還需要對文件進行改造
- 第一步:要實現(xiàn)三方so庫調(diào)用,在 calljnilib.h中聲明兩個和 calljnilib.cpp中對應(yīng)的方法:callJavaField和callJavaMethod,一般情況下這個頭文件是第三方庫一起提供的給外部調(diào)用的。
- 第二步:對CMakeLists配置文件改造。主要是做一些庫的配置操作。
- 第三步:編寫 third_call.cpp文件,在這內(nèi)部調(diào)用第三方庫。這里需要將第三方頭文件導(dǎo)入進來,如果CmakeLists文件中沒有聲明頭文件,就使用#include "include/calljnilib.h" 這種方式導(dǎo)入
- 第四步:最后測試下:callThirdSoMethod("com/yc/testjnilib/HelloCallBack","updateName");
5.4 Java動態(tài)調(diào)C++
- 先說一下靜態(tài)調(diào)C++的問題:
- 在實現(xiàn)stringFromJNI()時,可以看到c++里面的方法名很長 Java_com_yc_testjnilib_NativeLib_stringFromJNI。
- 這是jni靜態(tài)注冊的方式,按照jni規(guī)范的命名規(guī)則進行查找,格式為Java_類路徑_方法名。Studio默認這種方式名字太長了,能否設(shè)置短一點。
- 程序運行效率低,因為初次調(diào)用native函數(shù)時需要根據(jù)根據(jù)函數(shù)名在JNI層中搜索對應(yīng)的本地函數(shù),然后建立對應(yīng)關(guān)系,這個過程比較耗時。
- 動態(tài)注冊方法解決上面問題
- 當(dāng)程序在Java層運行System.loadLibrary("testjnilib");這行代碼后,程序會去載入testjnilib.so文件。
- 于此同時,產(chǎn)生一個Load事件,這個事件觸發(fā)后,程序默認會在載入的.so文件的函數(shù)列表中查找JNI_OnLoad函數(shù)并執(zhí)行。與Load事件相對,在載入的.so文件被卸載時,Unload事件被觸發(fā)。
- 此時,程序默認會去載入的.so文件的函數(shù)列表中查找JNI_OnLoad函數(shù)并執(zhí)行,然后卸載.so文件。
- 因此開發(fā)者經(jīng)常會在JNI_OnLoad中做一些初始化操作,動態(tài)注冊就是在這里進行的,使用env->RegisterNatives(clazz, gMethods, numMethods)。
- 動態(tài)注冊操作步驟:
- 第一步:因為System.loadLibrary()執(zhí)行時會調(diào)用此方法,實現(xiàn)JNI_OnLoad方法。
- 第二步:調(diào)用FindClass找到需要動態(tài)注冊的java類【定義要關(guān)聯(lián)的對應(yīng)Java類】,注意這個是native方法那個類的路徑字符串
- 第三步:定義一個靜態(tài)數(shù)據(jù)(JNINativeMethod類型),里面存放需要動態(tài)注冊的native方法,以及參數(shù)名稱
- 第四步:通過調(diào)用jni中的RegisterNatives函數(shù)將注冊函數(shù)的Java類,以及注冊函數(shù)的數(shù)組,以及個數(shù)注冊在一起,這樣就實現(xiàn)了綁定。
- 動態(tài)注冊優(yōu)勢分析
- 相比靜態(tài)注冊,動態(tài)注冊的靈活性更高,如果修改了native函數(shù)所在類的包名或類名,僅調(diào)整native函數(shù)的簽名信息即可。
- 還有一個優(yōu)勢:動態(tài)注冊,java代碼不需要更改,只需要更改native代碼。
- 效率更高:通過在.so文件載入初始化時,即JNI_OnLoad函數(shù)中,先行將native函數(shù)注冊到VM的native函數(shù)鏈表中去,后續(xù)每次java調(diào)用native函數(shù)時都會在VM中的native函數(shù)鏈表中找到對應(yīng)的函數(shù),從而加快速度。
06.一些技術(shù)原理
6.1 JNIEnv創(chuàng)建和釋放
- JNIEnv的創(chuàng)建方式
- C 中——JNIInvokeInterface:JNIInvokeInterface是C語言環(huán)境中的JavaVM結(jié)構(gòu)體,調(diào)用 (AttachCurrentThread)(JavaVM, JNIEnv*, void) 方法,能夠獲得JNIEnv結(jié)構(gòu)體;
- C<ins>中 ——_JavaVM:_JavaVM是C</ins>中JavaVM結(jié)構(gòu)體,調(diào)用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 方法,能夠獲取JNIEnv結(jié)構(gòu)體;
- JNIEnv的釋放:
- C 中釋放:調(diào)用JavaVM結(jié)構(gòu)體JNIInvokeInterface中的(DetachCurrentThread)(JavaVM)方法,能夠釋放本線程的JNIEnv
- C++ 中釋放:調(diào)用JavaVM結(jié)構(gòu)體_JavaVM中的jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 方法,就可以釋放 本線程的JNIEnv
- JNIEnv和線程的關(guān)系
- JNIEnv只在當(dāng)前線程有效:JNIEnv僅僅在當(dāng)前線程有效,JNIEnv不能在線程之間進行傳遞,在同一個線程中,多次調(diào)用JNI層方便,傳入的JNIEnv是同樣的
- 本地方法匹配多個JNIEnv:在Java層定義的本地方法,能夠在不同的線程調(diào)用,因此能夠接受不同的JNIEnv
6.2 動態(tài)注冊的原理
- 在Android源碼開發(fā)環(huán)境下,大多采用動態(tài)注冊native方法。
- 利用結(jié)構(gòu)體JNINativeMethod保存Java Native函數(shù)和JNI函數(shù)的對應(yīng)關(guān)系;
- 在一個JNINativeMethod數(shù)組中保存所有native函數(shù)和JNI函數(shù)的對應(yīng)關(guān)系;
- 在Java中通過System.loadLibrary加載完JNI動態(tài)庫之后,調(diào)用JNI_OnLoad函數(shù),開始動態(tài)注冊;
- JNI_OnLoad中會調(diào)用AndroidRuntime::registerNativeMethods函數(shù)進行函數(shù)注冊;
- AndroidRuntime::registerNativeMethods中最終調(diào)用jni RegisterNativeMethods完成注冊。
- 動態(tài)注冊原理分析
- RegisterNatives 方式的本質(zhì)是直接通過結(jié)構(gòu)體指定映射關(guān)系,而不是等到調(diào)用 native 方法時搜索 JNI 函數(shù)指針,因此動態(tài)注冊的 native 方法調(diào)用效率更高。
- 此外,還能減少生成 so 庫文件中導(dǎo)出符號的數(shù)量,則能夠優(yōu)化 so 庫文件的體積。
6.3 注冊JNI流程圖
-
提到了注冊 JNI 函數(shù)(建立 Java native 方法和 JNI 函數(shù)的映射關(guān)系)有兩種方式:靜態(tài)注冊和動態(tài)注冊。
- [圖片上傳失敗...(image-8ab717-1687914888789)]
-
分析下靜態(tài)注冊匹配 JNI 函數(shù)的執(zhí)行過程
- 第一步:以 loadLibrary() 加載 so 庫的執(zhí)行流程為線索進行分析的,最終定位到 FindNativeMethod() 這個方法。
- 第二步:查看
java_vm_ext.cc中FindNativeMethod方法,然后看到j(luò)ni_short_name和jni_long_name,獲取native方法對應(yīng)的短名稱和長名稱。 - 第三步:在
java_vm_ext.cc,通過FindNativeMethodInternal查找已經(jīng)加載的so庫中搜索,先搜索短名稱,然后再搜索長名稱 - 第四步:建立內(nèi)部數(shù)據(jù)結(jié)構(gòu),建立 Java native 方法與 JNI 函數(shù)的函數(shù)指針的映射關(guān)系,調(diào)用 native 方法,則直接調(diào)用已記錄的函數(shù)指針。
07.JNI遇到的問題
7.1 混淆的bug
-
在Android工程中要排除對native方法以及所在類的混淆(java工程不需要),否則要注冊的java類和java函數(shù)會找不到。proguard-rules.pro中添加。
# 設(shè)置所有 native 方法不被混淆 -keepclasseswithmembernames class * { native <methods>; } # 不混淆類 -keep class com.yc.testjnilib.** { *; }
7.2 注意字符串編譯
-
比如:對于JNI方法來說,使用如下方法返回或者調(diào)用直接崩潰了,有點搞不懂原理?
env->CallMethod(objCallBack,_methodName,"123"); -
這段代碼編譯沒問題,但是在運行的時候就報錯了:
JNI DETECTED ERROR IN APPLICATION: use of deleted global reference -
最終定位到是最后一個參數(shù)需要使用jstring而不能直接使用字符串表示。如下所示:
//思考一下,為什么直接返回字符串會出現(xiàn)錯誤提示?為何這樣設(shè)計…… //return "hello"; return env->NewStringUTF(hello.c_str());