本文章所用的工具版本
Android Studio 3.6.3
Gradle 5.6.4
NDK 21.3.6528147
CMake 3.10.2
什麼是 JNI?
JNI 的全稱是 Java Native Interface,從名稱上面翻譯,它是一個 Java 和 C 語言的接口,通過這個翻譯我們基本可以判定,這個 JNI 其實就是 Java 語言和 C 語言之間通訊的橋樑。
爲什麼要有 JNI?
因爲 Java 和 C 之間無法直接通訊,Java 和 JavaScript 也同理,無法直接通過代碼顯式調用,這中間需要一個翻譯官來做這件事,而 JNI 出現的目的就是爲了解決 Java 和 C 這兩個不同語言之間的通訊問題。
開胃菜
在正式進入主題之前,我們先講一下如何將一個普通的項目改造成一個 NDK 項目
創建一個 cpp 文件夾,這個文件夾和 java 是同級目錄
然後在這個文件夾下面創建一個 cpp 文件
cpp 文件其實就是 c++ 源碼,到了這裏可能大多數人又有一個疑問湧上心頭,剛剛不是說 Java 和 C,怎麼到這裏就變成 C++ 了呢?
這裏解釋一下,C++ 是 C 的超集,兼容大部分 C 語法,我們可以理解 C++ 是 C 的子類,擁有 C 的特性,同時又在這上面擴展了另外的一些特性。
那麼 C++ 相比 C 又有什麼不同呢?其實最大的不同在於,C 語法的設計思想是面向過程的,而 C++ 語法的設計思想是面向對象。
之所以用 C++ 而不用 C 的目的很簡單,Java 也是面向對象的語言,C++ 語言對於 Java 程序員來說比較容易接受,看 C++ 的代碼就像在看 Java 代碼差不多。
在 cpp 文件夾下再創建一個 CMake 文件
在 CMake 文件中配置一些 NDK 開發相關的參數
在 Gradle 中配置一些 CMake 相關的參數
到這裏就結束了?其實還有關鍵一步,如果我們沒有配置好的話,會直接導致我們無法對 C++ 的代碼進行斷點調試
在項目配置選擇 Debug 類型,Studio 提供了四種配置
Java Only:只斷點 Java 層的代碼
Native Only:只斷點 Native 層的代碼
Detect Automatically:自動檢測
Dual(Java + Native):兩種都用
默認是 Java Only,這樣會導致我們無法直接在項目中斷點 C/C++ 的代碼,所以在這裏我們應該選擇 Detect Automatically 或者 Dual(Java + Native)選項
到這裏就已經成功將一個普通的項目改造成 NDK 項目了,這只是一個開胃菜,接下來讓我們正式進入主題
主菜
我們創建一個 Java 類,在靜態代碼塊中加載 so 庫
需要注意的是:這裏的 so 庫的名稱不是根據 cpp 文件的名稱來定的,而是根據 CMake 中的配置而定的,只是現在爲了演示(偷懶),定義成同一個名稱而已。但是 so 庫生成的文件名稱最終會以 CMake 文件配置的爲準。
另外系統 API 給我們提供了兩種加載 so 的方式,第一種直接加載 apk 中的 so 文件,第二種是通過文件地址來加載 so 文件,一般情況下我們用第一種就可以了,第二種一般是在用在熱修復框架上面,它的實現方式也很簡單,通過修改靜態代碼塊中的代碼,將要加載的 so 的文件重新指向,加載目標從 apk 包轉移到應用的內部存儲中(data/data/包名/lib),在這之前熱修復框架會提前下載好 so 文件存放到此處。
爲了演示 Java 和 C++ 之間的相互調用,我們創建了兩個方法,第一個方法是 Java 調用 C++ 的代碼,第二個方法是 C++ 回調 Java 代碼
需要留意的是,Java 調用 C++ 的方法要被 native 修飾,表明這是一個本地方法,方法體不需要有任何實現
然後我們在 Native 層中創建一個跟 Java 層對應的方法
C++ 代碼?大多數人看到這裏就望而止步了,其實這裏面的代碼很簡單,接下來讓我們一步步解析這個 這些代碼的含義和作用
這個 include 在 Java 層上其實跟 import 差不多,但是在 C++ 文件中它不叫導包,而是叫引入頭文件
這塊我們可以理解成
Java 中的 JNI 方法要被 native 修飾,那 Native 層中的 JNI 方法同樣也不例外
需要特別留意的是,Native 層方法的返回值類型的定義位置有點奇特,和 Java 是不太一樣的,至於爲何 Java 上的返回值是 String 類型,而到了 Native 上的返回值卻是 jstring 類型,這個問題待會會講到。
Native 層中的 JNI 方法要和 Java 層中的 JNI 方法要對應上,在 Native 層中 JNI 方法的命名格式爲 Java_包名類名方法名,之所以用下劃線而不用小數點是因爲方法名不能帶特殊符號,無論是在 Java 代碼上還是 C/C++ 代碼上,這種情況都是不允許出現的,否則無法編譯通過。
接下來讓我們先看一下這兩個參數的含義,我相信大多數人的心裏已經有答案了
這個 jobject 其實就是外層的 Java 對象,具體是什麼對象,代碼提示已經告訴我們了
而 jstring 其實就是 Java 方法中傳入的參數,只不過在 Java 上叫 String,而在 Native 叫 jstring,參數這塊也是一一對應的
Java 類型 | JNI 別名 | C 類型 |
---|---|---|
boolean | jboolean | unsigned char |
byte | jbyte | signed char |
char | jchar | unsigned short |
short | jshort | short |
int | jint | int |
long | jlong | long |
float | jfloat | float |
double | jdouble | double |
String | jstring | char* |
Class | jclass | / |
Object | jobject | / |
我們先來看一張表,關於 Java 類型、JNI 別名、C 類型之間的對應表
由於 Java 和 C 語言之間無法直接調用,但是這兩種語言的基本數據類型是不一樣的,例如 Java 中有 boolean 類型, 而在 C 中就沒有這種類型,但是 C 語言還是有 if else 判斷的,那麼它是怎麼判斷 true 或者 false 的呢?正如表上所示,使用 char 類型,當 char 的值是 0 就是 false,非 0 就是 true。
兩種語言的數據結構存在巨大差異,基於這種情況,JNI 重新定義了一些類型,以便和 Java 上的類對應上,而這些類本質上還是屬於 C 語言中的類。
看了這幾句代碼,忽然心中出現一種似曾相識的感覺,但是始終說不出來是什麼
這種實現其實很類似於我們使用 Java 中的反射,屬於隱式調用,由於 Native 無法顯式調用 Java 代碼,所以也採用了隱式調用。而這裏面的 API 和 Java 的其實差不多,換湯不換藥,這裏不再多講。
JNIEnv 可以說是整個 JNI 的核心類,是 Java 和 C 通訊的橋樑,它可以協助我們將 JNI 類型轉換成 C 類型,不僅如此,調用 Java 對象的方法,獲取或者修改屬性,都是由 JNIEnv 來做。
JNIEnv 是一個結構體的一級指針,與其他類型的對象不一樣的地方是,類型後面帶了星號,使用的時候不能通過對象點方法名來調用,而是隻能通過對象->方法名來調用。
看完了普通 Java 方法調用 Native 方法,接下來看一下靜態 Java 方法是如何調用 Native 方法的
通過仔細對比,和之前的那種方式其實都差不多,但是有一個地方不太一樣
如果是 Java 層的 Native 方法是靜態的,那麼 Native 層中的方法第二個參數類型就是 jclass,這個 jclass 我們可以看做 Java 上面的 Class 類型。這種模式其實跟我們在 Java 方法體上面定義同步鎖的差不多,如果被 synchronized 修飾的方法是非靜態方法,那麼同步鎖的鎖對象就是
類名.this
,如果被 synchronized 修飾的方法是靜態方法,那麼同步鎖的鎖對象就是類名.class
上面就是 Java 和 Native 方法之間的互相調用,接下來讓我們簡單看一下 Native 層是如何獲取和修改 Java 對象的屬性值
這些代碼已經不用再講了,我相信大部分人都懂
甜點
char* 和 jstring 互轉
jstring string;
// jstring 轉 char*
const char* cc = env->GetStringUTFChars(string, 0);
// char* 轉 jstring
jstring ss = env->NewStringUTF(cc);
打印日誌
加入頭文件
#include <android/log.h>
打印 char*
const char* cc = "6666666";
__android_log_print(ANDROID_LOG_DEBUG, "TAG", cc, NULL);
日誌等級
ANDROID_LOG_VERBOSEANDROID_LOG_DEBUG
ANDROID_LOG_INFO
ANDROID_LOG_WARN
ANDROID_LOG_ERROR
作者:Android輪子哥
鏈接:https://www.jianshu.com/p/921a5142ae12
關注我獲取更多知識或者投稿