1. 程式人生 > 其它 >JNI 基礎用法相關總結

JNI 基礎用法相關總結

JNI

Sun JNI

JNI oracle 詳細文件

  • JNI overview

    • 為什麼需要 jni?
      • 有了標準以後,native 庫可移植。
        • 統一 java 和 native 的互操作介面,使得這個介面不會受具體的 JVM 實現的影響。早期有一些 JVM 規定了私有的與 native 的互動,JVM 之間的 native 操作不相容。
      • 一些時間要求嚴格的操作,使用低層次 native 程式碼進行,犧牲高階語言的方便,提高效能。
  • JNI 知識脈絡

    • 與 java 的互動
      • java 呼叫 native
        • jni 方法 load
        • 資料型別 / jni 方法格式
      • native 呼叫 java
        • jniEnv 提供的 API
          • 反射 : 真的反射 + (類 / 物件/string/array + 方法 / 屬性)
          • 鎖,JVM 資訊
  • JNI design

    • JNI 暴露給 native 的是一個 Pointer.Java 呼叫 native 的方法時,這個 Pointer 作為一個引數(即 JNIEnv,提供了很多操作 JVM 的方法,這些方法叫作 JNI 方法)

      • Pointer->Pointer->Pointer 是一個指標陣列,每個指標指向一個方法。
    • 編譯/ 連結 /載入

      • java 程式碼使用System.loadLibrary來載入 native 程式碼到記憶體中。JVM 內部為每一個classLoader維護一個已載入的 native library list.
    • JNI 方法名稱解析

      • Java_切割的全稱類名_切割的方法名[__如果方法過載,切割的方法引數簽名]
      • java 方法和 native 方法同名不算過載
      • Unicode 字元以及一些特別的符號(比如_ / ; / [等)會進行相應的轉義,轉義成_0/_1/...,因為方法名以及類名等不會以數字為開頭(所以 JNI 方法名的_數字不會有多重含義)。
    • native 方法引數

      • JNIEnv*是一個 JVM 指標,提供 JVM 對 native 的各種功能:access 物件,讀取 native 呼叫 JNI 產生的 exception 等。
      • 對於靜態 native 方法:f(JNIEnv*,jobject,args...)
        ,第二個引數為呼叫的 class
      • 對於非靜態方法:f(JNIEnv*,jobject,args...),第二個引數為呼叫的物件。
    • native 引用 JVM 物件

      • 基本型別資料是直接 copy value 的

      • 其餘型別傳遞引用到 native。所以

        1. 對 JVM 來說:JVM 必須對傳入 native 的物件引用(reference)做額外的計數,才能保證這些物件不被 gc 清除。
        2. 對 native 程式碼來說:native 程式碼必須在不需要引用後,主動通知 JVM。
      • 對於 native 程式碼來說,物件的 reference 有兩種:globallocal 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 程式碼持有的 local reference 和 global reference 都不會被 gc 掉。
      • native 程式碼獲取物件的 property 和呼叫物件的 method

        • 步驟
          1. jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);
          2. 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()來獲取是否另一個執行緒有異常發生,這裡我不能很好的理解。

    • JNI 資料型別

      • 基礎型別
        • 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 字元,節省空間。
    • JNI Functions

      • 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 Interfaceversion引數為要求的 JVM version,如果實際的 JVM 不支援指定的 version 的話(比如實際為1.1要求的卻是1.2),會返回錯誤,

Java 命令列中使用 jni

  1. 編寫 java/kt 程式碼,註冊 native 方法,在static程式碼塊中執行System.loadLibrary(對於 kt 為 companion object 的 init block).
  2. 通過 java/kt 程式碼生成 class 檔案
    • 使用javac或者kotlinc(在 AS 的 plugins 中有該工具),生成.class.
  3. 使用javah X.class對生成的.class檔案,生成所需的 C header file , .h
  4. 編寫 .h 對應的 .c 檔案,在其中實現方法宣告的方法。
  5. 呼叫 gcc -c X.c 來生成 .o 檔案
  6. 呼叫 gcc -shared -o X.so X.o 來生成 .so檔案,得到共享庫。(在 linux 上為libXXX.so,在 Mac 上為libXXX.jnilib
  7. 呼叫 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 等方法來獲取.