1.1 概念
JNI 全稱 Java Native Interface,Java 本地化接口,可以通過 JNI 調用系統提供的 API。操作系統,無論是 Linux,Windows 還是 Mac OS,或者一些彙編語言寫的底層硬件驅動都是 C/C++ 寫的。Java和C/C++不同 ,它不會直接編譯成平臺機器碼,而是編譯成虛擬機可以運行的Java字節碼的.class文件,通過JIT技術即時編譯成本地機器碼,所以有效率就比不上C/C++代碼,JNI技術就解決了這一痛點,JNI 可以說是 C 語言和 Java 語言交流的適配器、中間件,下面我們來看看JNI調用示意圖:來自JNI開發系列①JNI概念及開發流程 - 簡書
JNI技術通過JVM調用到各個平臺的API,雖然JNI可以調用C/C++,但是JNI調用還是比C/C++編寫的原生應用還是要慢一點,不過對高性能計算來說,這點算不得什麼,享受它的便利,也要承擔它的弊端。
1.2 JNI 與 NDK 區別
- JNI:JNI是一套編程接口,用來實現Java代碼與本地的C/C++代碼進行交互;
- NDK: NDK是Google開發的一套開發和編譯工具集,可以生成動態鏈接庫,主要用於Android的JNI開發;
2. JNI 作用
- 擴展:JNI擴展了JVM能力,驅動開發,例如開發一個wifi驅動,可以將手機設置爲無限路由;
- 高效: 本地代碼效率高,遊戲渲染,音頻視頻處理等方面使用JNI調用本地代碼,C語言可以靈活操作內存;
- 複用: 在文件壓縮算法 7zip開源代碼庫,機器視覺 OpenCV開放算法庫等方面可以複用C平臺上的代碼,不必在開發一套完整的Java體系,避免重複發明輪子;
- 特殊: 產品的核心技術一般也採用JNI開發,不易破解;
JNI在Android中作用:
JNI可以調用本地代碼庫(即C/C++代碼),並通過 Dalvik 虛擬機與應用層和應用框架層進行交互,Android中JNI代碼主要位於應用層和應用框架層;
- 應用層: 該層是由JNI開發,主要使用標準JNI編程模型;
- 應用框架層: 使用的是Android中自定義的一套JNI編程模型,該自定義的JNI編程模型彌補了標準JNI編程模型的不足;
補充知識點:
Java語言執行流程:
- 編譯字節碼:Java編譯器編譯 .java源文件,獲得.class 字節碼文件;
- 裝載類庫:使用類裝載器裝載平臺上的Java類庫,並進行字節碼驗證;
- Java虛擬機:將字節碼加入到JVM中,Java解釋器和即時編譯器同時處理字節碼文件,將處理後的結果放入運行時系統;
- 調用JVM所在平臺類庫:JVM處理字節碼後,轉換成相應平臺的操作,調用本平臺底層類庫進行相關處理;
Java一次編譯到處執行: JVM在不同的操作系統都有實現,Java可以一次編譯到處運行,字節碼文件一旦編譯好了,可以放在任何平臺的虛擬機上運行;
3. 查看 jni.h 文件源碼方法
jni.h 頭文件就是爲了讓 C/C++ 類型和 Java 原始類型相匹配的頭文件定義。
可以通過點擊 Android項目的含有#include <jni.h>
的頭文件或 C/C++ 文件跳轉到 jni.h 頭文件查看;
如果沒有這樣的文件的話,可以在 Android Studio 上新建一個類,隨便寫一個 native 方法,然後點擊紅色的方法,AS 會自動生成一個對應的 C 語言文件jnitest.c
,就可以找到 jni.h 文件了
或者,通過 javah 命令javah cn.cfanr.testjni.JniTest
,就可以生成對應頭文件cn_cfanr_testjni_JniTest.h
:
4. JNI 數據類型映射
由頭文件代碼可以看到,jni.h有很多類型預編譯的定義,並且區分了 C 和 C++的不同環境。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | typedef uint8_t jboolean; /* unsigned 8 bits */ typedef int8_t jbyte; /* signed 8 bits */ typedef uint16_t jchar; /* unsigned 16 bits */ typedef int16_t jshort; /* signed 16 bits */ typedef int32_t jint; /* signed 32 bits */ typedef int64_t jlong; /* signed 64 bits */ typedef float jfloat; /* 32-bit IEEE 754 */ typedef double jdouble; /* 64-bit IEEE 754 */ typedef unsigned char jboolean; /* unsigned 8 bits */ typedef signed char jbyte; /* signed 8 bits */ typedef unsigned short jchar; /* unsigned 16 bits */ typedef short jshort; /* signed 16 bits */ typedef int jint; /* signed 32 bits */ typedef long long jlong; /* signed 64 bits */ typedef float jfloat; /* 32-bit IEEE 754 */ typedef double jdouble; /* 64-bit IEEE 754 */ /* "cardinal indices and sizes" */ typedef jint jsize; /* * Reference types, in C++ */ class _jobject {}; class _jclass : public _jobject {}; class _jstring : public _jobject {}; class _jarray : public _jobject {}; class _jobjectArray : public _jarray {}; class _jbooleanArray : public _jarray {}; //…… typedef _jobject* jobject; typedef _jclass* jclass; typedef _jstring* jstring; typedef _jarray* jarray; typedef _jobjectArray* jobjectArray; typedef _jbooleanArray* jbooleanArray; //…… /* * Reference types, in C. */ typedef void* jobject; typedef jobject jclass; typedef jobject jstring; typedef jobject jarray; typedef jarray jobjectArray; typedef jarray jbooleanArray; //…… |
當是C++環境時,jobject, jclass, jstring, jarray 等都是繼承自_jobject
類,而在 C 語言環境是,則它的本質都是空類型指針typedef void* jobject;
4.1 基本數據類型
下圖是Java基本數據類型和本地類型的映射關係,這些基本數據類型都是可以直接在 Native 層直接使用的:
4.2 引用數據類型
另外,還有引用數據類型和本地類型的映射關係:
需要注意的是,
- 1)引用類型不能直接在 Native 層使用,需要根據 JNI 函數進行類型的轉化後,才能使用;
- 2)多維數組(含二維數組)都是引用類型,需要使用 jobjectArray 類型存取其值;
例如,二維整型數組就是指向一位數組的數組,其聲明使用方式如下:
1 2 3 4 | //獲得一維數組的類引用,即jintArray類型 jclass intArrayClass = env->FindClass("[I"); //構造一個指向jintArray類一維數組的對象數組,該對象數組初始大小爲length,類型爲 jsize jobjectArray obejctIntArray = env->NewObjectArray(length ,intArrayClass , NULL); |
4.3 方法和變量 ID
同樣不能直接在 Native 層使用。當 Native 層需要調用 Java 的某個方法時,需要通過 JNI 函數獲取它的 ID,根據 ID 調用 JNI 函數獲取該方法;變量的獲取也是類似。ID 的結構體如下:
1 2 3 4 5 | struct _jfieldID; /* opaque structure */ typedef struct _jfieldID* jfieldID; /* field IDs */ struct _jmethodID; /* opaque structure */ typedef struct _jmethodID* jmethodID; /* method IDs */ |
5. JNI 描述符
5.1域描述符
1)基本類型描述符
下面是基本的數據類型的描述符,除了 boolean 和 long 類型分別是 Z 和 J 外,其他的描述符對應的都是Java類型名的大寫首字母。另外,void 的描述符爲 V
2)引用類型描述符
一般引用類型描述符的規則如下,注意不要丟掉“;”
1 | L + 類描述符 + ; |
如,String 類型的域描述符爲:
1 | Ljava/lang/String; |
數組的域描述符特殊一點,如下,其中有多少級數組就有多少個“[”,數組的類型爲類時,則有分號,爲基本類型時沒有分號
1 | [ + 其類型的域描述符 |
例如:
1 2 3 4 5 6 | int[] 描述符爲 [I double[] 描述符爲 [D String[] 描述符爲 [Ljava/lang/String; Object[] 描述符爲 [Ljava/lang/Object; int[][] 描述符爲 [[I double[][] 描述符爲 [[D |
對應在 jni.h 獲取 Java 的字段的 native 函數如下,name爲 Java 的字段名字,sig 爲域描述符
1 2 3 4 5 6 7 8 | //C jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*); jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID); //C++ jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) { return functions->GetFieldID(this, clazz, name, sig); } jobject GetObjectField(jobject obj, jfieldID fieldID) { return functions->GetObjectField(this, obj, fieldID); } |
具體使用,後面會講到
5.2 類描述符
類描述符是類的完整名稱:包名+類名,java 中包名用 . 分割,jni 中改爲用 / 分割
如,Java 中 java.lang.String 類的描述符爲 java/lang/String
native 層獲取 Java 的類對象,需要通過 FindClass() 函數獲取, jni.h 的函數定義如下:
1 2 3 4 5 | //C jclass (*FindClass)(JNIEnv*, const char*); //C++ jclass FindClass(const char* name) { return functions->FindClass(this, name); } |
字符串參數就是類的引用類型描述符,如 Java 對象 cn.cfanr.jni.JniTest,對應字符串爲Lcn/cfanr/jni/JniTest; 如下:
1 | jclass jclazz = env->FindClass("Lcn/cfanr/jni/JniTest;"); |
詳細用法的例子,後面會講到。
5.3 方法描述符
方法描述符需要將所有參數類型的域描述符按照聲明順序放入括號,然後再加上返回值類型的域描述符,其中沒有參數時,不需要括號,如下規則:
1 | (參數……)返回類型 |
例如:
1 2 3 4 | Java 層方法 ——> JNI 函數簽名 String getString() ——> Ljava/lang/String; int sum(int a, int b) ——> (II)I void main(String[] args) ——> ([Ljava/lang/String;)V |
另外,對應在 jni.h 獲取 Java 方法的 native 函數如下,其中 jclass 是獲取到的類對象,name 是 Java 對應的方法名字,sig 就是上面說的方法描述符
1 2 3 4 5 | //C jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); //C++ jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) { return functions->GetMethodID(this, clazz, name, sig); } |
不過在實際編程中,如果使用 javah 工具來生成對應的 native 代碼,就不需要手動編寫對應的類型轉換了。
6. JNIEnv 分析
JNIEnv 是 jni.h 文件最重要的部分,它的本質是指向函數表指針的指針(JavaVM也是),函數表裏面定義了很多 JNI 函數,同時它也是區分 C 和 C++環境的(由上面介紹描述符時也可以看到),在 C 語言環境中,JNIEnv 是strut JNINativeInterface*
的指針別名。
1 2 3 4 5 6 7 8 9 10 11 | struct _JNIEnv; struct _JavaVM; typedef const struct JNINativeInterface* C_JNIEnv; typedef _JNIEnv JNIEnv; //C++中的 JNIEnv 類型 typedef _JavaVM JavaVM; typedef const struct JNINativeInterface* JNIEnv; //C語言的 JNIEnv 類型 typedef const struct JNIInvokeInterface* JavaVM; |
6.1 JNIEnv 特點
- JNIEnv 是一個指針,指向一組 JNI 函數,通過這些函數可以實現 Java 層和 JNI 層的交互,就是說通過 JNIEnv 調用 JNI 函數可以訪問 Java 虛擬機,操作 Java 對象;
- 所有本地函數都會接收 JNIEnv 作爲第一個參數;(不過 C++ 的JNI 函數已經對 JNIEnv 參數進行了封裝,不用寫在函數參數上)
- 用作線程局部存儲,不能在線程間共享一個 JNIEnv 變量,也就是說 JNIEnv 只在創建它的線程有效,不能跨線程傳遞;相同的 Java 線程調用本地方法,所使用的 JNIEnv 是相同的,一個 native 方法不能被不同的 Java 線程調用;
6.2 JavaEnv 和 JavaVM 的關係
- 1)每個進程只有一個 JavaVM(理論上一個進程可以擁有多個 JavaVM 對象,但 Android 只允許一個),每個線程都會有一個 JNIEnv,大部分 JNIAPI 通過 JNIEnv 調用;也就是說,JNI 全局只有一個 JavaVM,而可能有多個 JNIEnv;
- 2)一個 JNIEnv 內部包含一個 Pointer,Pointer 指向 Dalvik 的 JavaVM 對象的 Function Table,JNIEnv 內部的函數執行環境來源於 Dalvik 虛擬機;
- 3)Android 中每當一個Java 線程第一次要調用本地 C/C++ 代碼時,Dalvik 虛擬機實例會爲該 Java 線程產生一個 JNIEnv 指針;
- 4)Java 每條線程在和 C/C++ 互相調用時,JNIEnv 是互相獨立,互不干擾的,這樣就提升了併發執行時的安全性;
- 5)當本地的 C/C++ 代碼想要獲得當前線程所想要使用的 JNIEnv 時,可以使用 Dalvik VM 對象的 JavaVM jvm->GetEnv()方法,該方法會返回當前線程所在的 JNIEnv;
- 6)Java 的 dex 字節碼和 C/C++ 的 .so 同時運行 Dalvik VM 之內,共同使用一個進程空間;
6.3 C 語言的 JNIEnv
由上面代碼可知,C 語言的JNIEnv 就是const struct JNINativeInterface*
,而 JNIEnv* env
就等價於JNINativeInterface** env
,env 實際是一個二級指針,所以想要得到 JNINativeInterface 結構體中定義的函數指針,就需要先獲取 JNINativeInterface 的一級指針對象env,然後才能通過一級指針對象調用 JNI 函數,例如:
`(env)->NewStringUTF(env, “hello”)`
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct JNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void* reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize); jclass (*FindClass)(JNIEnv*, const char*); jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); jfieldID (*FromReflectedField)(JNIEnv*, jobject); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean); jclass (*GetSuperclass)(JNIEnv*, jclass); jboolean (*IsAssignableFrom)(JNIEnv*, jclass, jclass); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedField)(JNIEnv*, jclass, jfieldID, jboolean); //……定義了一系列關於 Java 操作的函數 } |
6.4 C++的 JNIEnv
由typedef _JNIEnv JNIEnv;
可知,C++的 JNIEnv 是 _JNIEnv 結構體,而 _JNIEnv 結構體定義了 JNINativeInterface 的結構體指針,內部定義的函數實際上是調用 JNINativeInterface 的函數,所以C++的 env 是一級指針,調用時不需要加 env 作爲函數的參數,例如:env->NewStringUTF(env, "hello")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | struct _JNIEnv { /* do not rename this; it does not seem to be entirely opaque */ const struct JNINativeInterface* functions; jint GetVersion() { return functions->GetVersion(this); } jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen) { return functions->DefineClass(this, name, loader, buf, bufLen); } jclass FindClass(const char* name) { return functions->FindClass(this, name); } jmethodID FromReflectedMethod(jobject method) { return functions->FromReflectedMethod(this, method); } jfieldID FromReflectedField(jobject field) { return functions->FromReflectedField(this, field); } jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic) { return functions->ToReflectedMethod(this, cls, methodID, isStatic); } jclass GetSuperclass(jclass clazz) { return functions->GetSuperclass(this, clazz); } //…… } |
7. JNI 的兩種註冊方式
Java 的 native 方法是如何鏈接 C/C++中的函數的呢?可以通過靜態和動態的方式註冊JNI。
7.1 靜態註冊
原理:根據函數名建立 Java 方法和 JNI 函數的一一對應關係。流程如下:
- 先編寫 Java 的 native 方法;
- 然後用 javah 工具生成對應的頭文件,執行命令
javah packagename.classname
可以生成由包名加類名命名的 jni 層頭文件,或執行命名javah -o custom.h packagename.classname
,其中 custom.h 爲自定義的文件名; - 實現 JNI 裏面的函數,再在Java中通過System.loadLibrary加載 so 庫即可;
靜態註冊的方式有兩個重要的關鍵詞 JNIEXPORT 和 JNICALL,這兩個關鍵詞是宏定義,主要是註明該函數式 JNI 函數,當虛擬機加載 so 庫時,如果發現函數含有這兩個宏定義時,就會鏈接到對應的 Java 層的 native 方法。
由前面3. 查看 jni.h 文件源碼方法
生成頭文件的方法,重新創建一個cn.cfanr.test_jni.Jni_Test.java
的類
1 2 3 4 5 6 7 8 9 10 11 | public class Jni_Test { private static native int swap(); private static native void swap(int a, int b); private static native void swap(String a, String b); private native void swap(int[] arr, int a, int b); private static native void swap_0(int a, int b); } |
用 javah 工具生成以下頭文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | /* Header for class cn_cfanr_test_jni_Jni_Test */ extern "C" { /* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: ()I */ JNIEXPORT jint JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__ (JNIEnv *, jclass); // 凡是重載的方法,方法後面都會多一個下劃線 /* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: (II)V */ JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__II (JNIEnv *, jclass, jint, jint); /* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: (Ljava/lang/String;Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__Ljava_lang_String_2Ljava_lang_String_2 (JNIEnv *, jclass, jstring, jstring); /* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: ([III)V */ JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap___3III (JNIEnv *, jobject, jintArray, jint, jint); // 非 static 的爲 jobject /* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap_0 * Signature: (II)V */ JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap_10 (JNIEnv *, jclass, jint, jint); // 不知道爲什麼後面沒有 II } |
可以看出 JNI 的調用函數的定義是按照一定規則命名的:JNIEXPORT 返回值 JNICALL Java_全路徑類名_方法名_參數簽名(JNIEnv* , jclass, 其它參數);
其中 Java_ 是爲了標識該函數來源於 Java。經檢驗(不一定正確),如果是重載的方法,則有“參數簽名”,否則沒有;另外如果使用的是 C++,在函數前面加上 extern “C”(表示按照 C 的方式編譯),函數命名後面就不需要加上“參數簽名”。
另外還需要注意幾點特殊規則:參考:官方JNI規範翻譯 | linlinjava的博客 2.2.1 本地方法名解析
- 1. 包名或類名或方法名中含下劃線 _ 要用 _1 連接;
- 2. 重載的本地方法命名要用雙下劃線 __ 連接;
- 3. 參數簽名的斜槓 “/” 改爲下劃線 “_” 連接,分號 “;” 改爲 “_2” 連接,左方括號 “[” 改爲 “_3” 連接;
另外,對於 Java 的 native 方法,static 和非 static 方法的區別在於第二個參數,static 的爲 jclass,非 static 的 爲 jobject;JNI 函數中是沒有修飾符的。
優點:
實現比較簡單,可以通過 javah 工具將 Java代碼的 native 方法直接轉化爲對應的native層代碼的函數;
缺點:
- javah 生成的 native 層函數名特別長,可讀性很差;
- 後期修改文件名、類名或函數名時,頭文件的函數將失效,需要重新生成或手動改,比較麻煩;
- 程序運行效率低,首次調用 native 函數時,需要根據函數名在 JNI 層搜索對應的本地函數,建立對應關係,有點耗時;
7.2 動態註冊
原理:直接告訴 native 方法其在JNI 中對應函數的指針。通過使用 JNINativeMethod 結構來保存 Java native 方法和 JNI 函數關聯關係,步驟:
- 先編寫 Java 的 native 方法;
- 編寫 JNI 函數的實現(函數名可以隨便命名);
- 利用結構體 JNINativeMethod 保存Java native方法和 JNI函數的對應關係;
- 利用
registerNatives(JNIEnv* env)
註冊類的所有本地方法; - 在 JNI_OnLoad 方法中調用註冊方法;
- 在Java中通過System.loadLibrary加載完JNI動態庫之後,會調用JNI_OnLoad函數,完成動態註冊;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //JNINativeMethod結構體 typedef struct { const char* name; //Java中native方法的名字 const char* signature; //Java中native方法的描述符 void* fnPtr; //對應JNI函數的指針 } JNINativeMethod; /** * @param clazz java類名,通過 FindClass 獲取 * @param methods JNINativeMethod 結構體指針 * @param nMethods 方法個數 */ jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) //JNI_OnLoad JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved); |