JNI 基礎用法相關總結
JNI
Sun JNI
-
JNI overview
- 為什麼需要 jni?
- 有了標準以後,native 庫可移植。
- 統一 java 和 native 的互操作介面,使得這個介面不會受具體的 JVM 實現的影響。早期有一些 JVM 規定了私有的與 native 的互動,JVM 之間的 native 操作不相容。
- 一些時間要求嚴格的操作,使用低層次 native 程式碼進行,犧牲高階語言的方便,提高效能。
- 有了標準以後,native 庫可移植。
- 為什麼需要 jni?
-
JNI 知識脈絡
- 與 java 的互動
- java 呼叫 native
- jni 方法 load
- 資料型別 / jni 方法格式
- native 呼叫 java
- jniEnv 提供的 API
- 反射 : 真的反射 + (類 / 物件/string/array + 方法 / 屬性)
- 鎖,JVM 資訊
- jniEnv 提供的 API
- java 呼叫 native
- 與 java 的互動
-
JNI design
-
JNI 暴露給 native 的是一個 Pointer.Java 呼叫 native 的方法時,這個 Pointer 作為一個引數(即 JNIEnv,提供了很多操作 JVM 的方法,這些方法叫作 JNI 方法)
- Pointer->Pointer->Pointer 是一個指標陣列,每個指標指向一個方法。
-
編譯/ 連結 /載入
- java 程式碼使用
System.loadLibrary
來載入 native 程式碼到記憶體中。JVM 內部為每一個classLoader
維護一個已載入的 native library list.
- java 程式碼使用
-
JNI 方法名稱解析
Java_切割的全稱類名_切割的方法名[__如果方法過載,切割的方法引數簽名]
- java 方法和 native 方法同名不算過載
- Unicode 字元以及一些特別的符號(比如
_
/;
/[
等)會進行相應的轉義,轉義成_0
/_1
/...,因為方法名以及類名等不會以數字為開頭(所以 JNI 方法名的_數字
不會有多重含義)。
-
native 方法引數
JNIEnv*
是一個 JVM 指標,提供 JVM 對 native 的各種功能:access 物件,讀取 native 呼叫 JNI 產生的 exception 等。- 對於靜態 native 方法:
f(JNIEnv*,jobject,args...)
- 對於非靜態方法:
f(JNIEnv*,jobject,args...)
,第二個引數為呼叫的物件。
-
native 引用 JVM 物件
-
基本型別資料是直接 copy value 的
-
其餘型別傳遞引用到 native。所以
- 對 JVM 來說:JVM 必須對傳入 native 的物件引用(reference)做額外的計數,才能保證這些物件不被 gc 清除。
- 對 native 程式碼來說:native 程式碼必須在不需要引用後,主動通知 JVM。
-
對於 native 程式碼來說,物件的 reference 有兩種:global和local refs
- global reference
- 由 native 程式碼儲存的 JVM 物件引用。
- 由 native 程式碼呼叫
globalRef = (*env)->NewGlobalRef(env, localRef)
申請,呼叫(*env)->DeleteGlobalRef(env, globalRef)
宣告不再引用。
- local reference 。 沒有主動呼叫的型別
- 存在週期:從 native 程式碼開始到 native 程式碼返回。JVM 在呼叫 native 方法時主動維護一個 local reference table,儲存所有 native 程式碼引用的 local reference 防止其物件被 gc 掉,在 native 方法結束後將這些 reference 清空,允許 gc 清除。native 程式碼可以主動呼叫
(*env)->DeleteLocalRef(env, localRef)
來允許 JVM 回收這個物件。 - 包括範圍
- 傳入 native 程式碼的引數;
- native 程式碼返回的引數。
- 包不包括 native 程式碼請求 JVM 生成的物件呢?
- 存在週期:從 native 程式碼開始到 native 程式碼返回。JVM 在呼叫 native 方法時主動維護一個 local reference table,儲存所有 native 程式碼引用的 local reference 防止其物件被 gc 掉,在 native 方法結束後將這些 reference 清空,允許 gc 清除。native 程式碼可以主動呼叫
- 被 native 程式碼持有的 local reference 和 global reference 都不會被 gc 掉。
- global reference
-
native 程式碼獲取物件的 property 和呼叫物件的 method
- 步驟
jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
- 獲取到
methodId
後,可以繼續繼續呼叫env->CallXXXMethod
來呼叫,但是如果呼叫時這個 class 已經被 JVM unload 了,會產生問題。所以最好每次都GetMethodId
後再CallXXXMethod
.
- 步驟
-
-
native 程式碼呼叫 JNI 方法時,JNI 方法可能會丟擲錯誤。native 程式碼應該在呼叫 JNI 方法後,呼叫
ExceptionOccurred()
來檢查是不是發生了錯誤,獲取 pendingException,並且做相應的處理。如果沒有處理 pendingException 就呼叫其他的 JNI 方法,即有可能會發生錯誤,只有少數幾個 JNI 方法在有 pendingException 的情況下可以正常執行 -
native 程式碼呼叫 JNI 方法
ExceptionClear()
來表示 pendingException 已經被處理了。 -
native 程式碼 return 的時候,有未處理的 pendingException,會在 java 程式碼裡 raise this pendingException
-
native 程式碼可以呼叫 JNI 方法
ThrowNew(JNIEnv*, exceptionClassObject, exceptionMsgString)
來主動 raise an exception. -
多執行緒時的異常處理。?一個執行緒需要在適當的時候呼叫
ExceptionOccurred()
來獲取是否另一個執行緒有異常發生,這裡我不能很好的理解。
-
-
-
- 基礎型別
- boolean -> jboolean;
- byte -> jbyte
- ...
- 引用型別
jobject / jclass / jstring / jarray / jXXXarray / jthrowable
- C++裡面這些型別有父子繼承關係,C 裡面都是 jobject.
- type signature
- 基礎型別:
Zboolean Bbyte Cchar Sshort Iint Jlong Ffloat Ddouble
- 類:
L fully-qualified-class ;
,比如Ljava/lang/String;
- 陣列:
[type
,比如[B
- 方法:
( arg-types ) ret-type
,比如(ILjava/lang/String;[I)J
- 基礎型別:
- Modified UTF-8 String
- 一個 UTF-8 字元佔 2 個 byte,Modified UTF-8 字元根據 UTF-8 的字元程式碼,佔據 1 個 byte(ASCII),或者兩個,或者三個。這裡涉及到了編碼的設計。
- 這個 UTF-8 的設計可以使得 ASCII 字元只佔據一個 byte,正式 UTF-8 佔兩個 byte,對於 ASCII 字元,節省空間。
- 基礎型別
-
JNIEnv*
是一個指標,指向一個包含所有 JNI 方法指標的struct
。- functions 分類具體的 jni functions.
- Version Information
- Class Operations
- Exceptions
- Global and Local References
- Weak Global References
- Object Operations
- Accessing Fields of Objects
- Calling Instance Methods
- Accessing Static Fields
- Calling Static Methods
- String Operations
- Array Operations
- Registering Native Methods
- Monitor Operations
- NIO Support
- Reflection Support
- Java VM Interface
-
Invocation API 。 用來給獨立的 native 程式碼(即不是從 java 的
System.loadLibrary
載入的 native 程式碼)操作 JVM 的 API-
可以主動新建一個
JVM
;讓 JVM 載入一個指定的class
;執行類的某些方法或者進行某些操作(就像一般的 native 程式碼執行 jvm 方法一樣)。 -
JVM 載入 native library
- jdk1.2 後,native library 跟自己類所在的 classLoader 繫結。一旦所在類的 classLoader 被解除安裝了,native library 也會被清除;一個 JVM 只能載入一個 native library 一次
- native library 可以提供一個方法
jint JNI_OnLoad(JavaVM *vm, void *reserved);
在System.loadLibrary
時 JVM 主動呼叫,以獲取 native library 要求的 JVM 版本號(比如JNI_VERSION_1_2
這些都是已定義好的int
常量)。 - native library 可以提供一個方法
void JNI_OnUnload(JavaVM *vm, void *reserved);
在包含 native library 的 class loader 被 gc 的時候由 JVM 主動呼叫,以讓 native code 執行一些必要的記憶體清理工作(比如釋放 global reference 等)。
-
Invocation API functions
native 可以用來主動操作 JVM 的方法(全域性方法,不需要呼叫env->XXX
,需要在 native code 裡#include <jni.h>
)。jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);
native code 呼叫這個方法來獲取 JVM 的預設配置引數,vm_args
是一個指向JavaVMInitArgs
結構的指標。
// JavaVMInitArgs結構 typedef struct JavaVMInitArgs { jint version; jint nOptions; JavaVMOption *options; jboolean ignoreUnrecognized; } JavaVMInitArgs; // JavaVMOption typedef struct JavaVMOption { char *optionString; /* the option as a string in the default platform encoding */ void *extraInfo; } JavaVMOption; // JavaVM結構 typedef const struct JNIInvokeInterface *JavaVM; const struct JNIInvokeInterface ... = { NULL, NULL, NULL, DestroyJavaVM, AttachCurrentThread, DetachCurrentThread, GetEnv, AttachCurrentThreadAsDaemon };
-
從 JDK1.2 開始,不支援一個程序裡有多個 JVM 了。所以下面的幾個方法都受到影響(比如只能獲得一個 JVM,或者是)
jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
獲取建立的所有 JVM,放到vmBuf
裡。jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
新建一個JavaVM
,native code 所在的執行緒將會是 JVM 的主執行緒,引數p_env
會用來放 JVM 主執行緒的JNI Interface
,引數vm_args
是指向JavaVMInitArgs
結構的指標。
-
Java VM
結構方法表中的方法:jint DestroyJavaVM(JavaVM *vm);
將當前執行緒 attatch 到 JVM 上,在該執行緒成為 JVM 的唯一使用者執行緒時,退出該 JVM 並且釋放其佔有的資源。jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
把當前執行緒加到 JVM 裡,引數p_env
接收加進去以後的JNI Interface
的指標,thr_args
是增加執行緒到 JVM 的引數,結構如下:
typedef struct JavaVMAttachArgs { jint version; /* must be at least JNI_VERSION_1_2 */ char *name; /* the name of the thread as a modified UTF-8 string, or NULL */ jobject group; /* global ref of a ThreadGroup object, or NULL */ } JavaVMAttachArgs
jint AttachCurrentThreadAsDaemon(JavaVM* vm, void** p_env, void* args);
把當前執行緒作為 Daemon thread加到 JVM 裡。如果已經加過了,單純地設定p_env
的值,不會改變已經新增的執行緒的 daemon 狀態(即如果之前不是 daemon,呼叫這個方法並不會讓其變成 daemon)jint DetachCurrentThread(JavaVM *vm);
取消 JVM 裡的當前執行緒,其佔有的鎖都會釋放掉。jint GetEnv(JavaVM *vm, void **env, jint version);
獲取 JVM 中當前執行緒對應的JNI Interface
,version
引數為要求的 JVM version,如果實際的 JVM 不支援指定的 version 的話(比如實際為1.1
要求的卻是1.2
),會返回錯誤,
-
-
Java 命令列中使用 jni
- 編寫 java/kt 程式碼,註冊 native 方法,在
static
程式碼塊中執行System.loadLibrary
(對於 kt 為 companion object 的init
block). - 通過 java/kt 程式碼生成 class 檔案
- 使用
javac
或者kotlinc
(在 AS 的 plugins 中有該工具),生成.class
.
- 使用
- 使用
javah X.class
對生成的.class
檔案,生成所需的 C header file ,.h
- 編寫
.h
對應的.c
檔案,在其中實現方法宣告的方法。 - 呼叫
gcc -c X.c
來生成.o
檔案 - 呼叫
gcc -shared -o X.so X.o
來生成.so
檔案,得到共享庫。(在 linux 上為libXXX.so
,在 Mac 上為libXXX.jnilib
) - 呼叫
java
執行有 jni 參與的 java 類。- 使用
-Djava.library.path=""
來引用所有的 jni 庫 - 使用
-cp
來指定所需的 class 或者 jar. - java_command.md
- 使用
問題
何時載入 so
在呼叫 native
方法前的任何時間都可以.通常在類的 static
程式碼塊中進行載入.
jni 方法是如何進行註冊的.
- 靜態註冊 : 通過
javah
生成.h
檔案,實現其中的方法. 優點: 簡單; 缺點 : 方法名長. - 動態註冊 : 通過在
jNI_OnLoad
方法中呼叫JNIEnv.registerNatives
來進行註冊,其中引數有java 方法名
和c 方法指標
的對應.
jni 的 java 層和 c 層的引數型別如何轉換? Integer 會轉成什麼型別? string 呢?
- 除了 string/class/Throwable 外的 Object 都轉成
jobject
. - Integer 是 jobject , string 是 jstring
JNIEnv 是執行緒相關的嗎?
- 是的, JNIEnv 是執行緒獨立的.
JNI 如何在 native 呼叫 java 的方法? 如何獲取一個物件的屬性?
- 找到 jclass -> 通過
jniEnv.getMethodId(jclass, methodName, methodSig)
獲取 jMethodID -> 通過jniEnv.callVoidMethod(obj, methodId, params)
;
對於static
方法要使用jniEnv.callStaticVoidMethod
(可能是因為涉及到方法的分派) - 物件的成員變數需要用
getByyteField
等方法來獲取.