NDK 開發:JNI 互調

本文章所用的工具版本

  • 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 類型
booleanjbooleanunsigned char
bytejbytesigned char
charjcharunsigned short
shortjshortshort
intjintint
longjlonglong
floatjfloatfloat
doublejdoubledouble
Stringjstringchar*
Classjclass/
Objectjobject/
  • 我們先來看一張表,關於 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

關注我獲取更多知識或者投稿

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章