--- title: 'JNI 機制' disqus: kyleAlien --- JNI 機制 === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: **Java 語言跟 C 語言是兩種不相同的語言(或其他語言),Java 並不源於 C,所以必須透過==中間語言==翻譯,而該語言就是 JNI** [TOC] ## JNI(Java Native Interface) * **JNI 是一個翻譯語言**,形成原因是因為 Java 運行一個程序需要跟許多系統平台溝通,**==JNI 是 java 跨平台的基礎==** * **在 Java 中而使用 native 修飾後說明這個方法是透過 JNI 實現的**,它是透過本地系統中的 api 方法來實現,這方法可能是 C/C++ or **其他語言** (並不只有 C/C++) > ![](https://i.imgur.com/Atgleop.png) ### JNI 使用時機 1. 應用程序需要一些平台相關的 feature 支持,像是 藍芽、WIFI ... 2. 兼容使用其他語言寫的 Lib,**程式實現復用** 3. 快速調用,越接近底層的語言(C、C++、彙編) 程式運行的速度越塊 ### Android JNI * 簡單創建 C/C++ 的專案,是最快開啟 JNI 使用大門的方法 > New -> New Project -> Native C/C++ > > ![](https://i.imgur.com/BkYZiMh.png) ### 交叉編譯 * 在不同平台上 (window/mac/linux) 編譯出 android 系統可執行的文件 > 當使用到音視頻開發 `ffmpeg` / `openGIEs` / `openCV` 就需要使用不同平台編譯出靜態庫 or 動態庫使用 * 簡單來說就是用自己平台以外的其他平台編譯該平台可用的庫,**很多 Android 庫是在 Linux 上編譯**的 (因為 Android 基於 Linux 系統) ### 動態庫 & 靜態庫 * 動態庫 & 靜態庫的特性是相反的 | 庫-類型 (Linux) | 特性 | 操作 | | -------- | -------- | -------- | | `.a` 靜態庫 | 當其中一個函數改變,所有函數庫都要重編,**升級速度較慢,但調用速度較快,容量大** | 將函數庫的所有數據都整合進目標代碼中 | | `.so` 動態庫 | 程序的環境中必須提供相應的庫,**改變升級較快,但調用速度較慢,容量小** | 編譯期並不會加入目標代碼中,當有需要時才載入 | * Android 載入庫是使用 `CMakeList.txt` 載入,加入 `add_library`(libname 動態/靜態 庫),可參考 [**Android NDK**](https://hackmd.io/NOzQJQo4R0i5iVAD9tVyxw#設置-CMake-script) 開發 ## Java 使用 Native 函數 * 要在 Java 程序中使用本地函數有以下規則 1. 需要靜態加載 `so`、`dll` 厙 2. 在本地需要實現的 java 方法加上 `native` 聲明 ```java= public class jniTest { private String H = "Java Hello"; // 加載動態厙 static { System.load("//home//aliens//C//helloWorld.so"); } public jniTest() { System.out.println("Jni Constructor"); } // 本地方法宣告 public native String HelloWorld(); public static void main(String[] args) { jniTest t = new jniTest(); String str = t.HelloWorld(); System.out.println("native Func: " + str); } } ``` ### Java 簽名標籤 * 主要先關注 `引數` 再關注 `返回類型`,引數要用括號,並連接返回值,**中間++不能有空格++** * Java 函數,簽名格式,**(引數)返回** | Java Type | 簽名 | | -------- | -------- | | boolean | ==Z== | | byte | B | | char | C | | short | S | | int | I | | long | ==J== | | float | F | | double | D | | class | ==L+全類名;== (記得分號)| | array | ==\[type== | | void | V | ```shell= ## java 函數 1 String MyString(Long long, String str); ## 簽名 1 (JLjava/lang/String;)Ljava/lang/String; ## java 函數 2 void main(String[] str); ## 簽名 2 ([Ljava/lang/String;)V; ## java 函數 2 int Function(int i, String s, long[] l); ## 簽名 2 (ILjava/lang/String;[J)I; ``` * **java 命令** 1. `javac jniTest.java`,首先先將 java 檔案編譯 2. **++javap -s -p jniTest.class++**,用此命令就可以直接呼叫出簽名 **(-p 代表私有類,-s 代表簽名,可使用 javap -help 查看)**,**該命令++只能查看 class++** **--java 指令操作--** > 可看到 **`javap` 指令只能針對 class file**,無法操作在 `.java` 檔案 > > ![](https://i.imgur.com/nUHMSnu.png) ### 基礎類型的映射 * Java to Jni 數據類型的映射,如下表 | java 類型 | jni 類型 | 原型描述 | | -------- | -------- | -------- | | boolean | jboolean | **unsigned** **8 bits** | | byte | jbyte | **signed** 8 bits | | char | jchar | **unsigned** 8 bits | | short | jshort | signed 16 bits | | int | jint | signed 32 bits | | float | jfloat | signed 32 bits | | double | jdouble | signed 64 bits | | Class | jclass | class 對象 | | String | jstring | 字符串對象 | | Object | jobject | 任何 java 對象 | | void | void | void | ### JNI 引數映射表 * 除了上面的類型,針對引數還有一些不同的,尤其針對每一種類型的 array 就有不相同映射 | Java 類型 | JNI 類型 | | -------- | -------- | | **All Object** (任意對象) | ==jobject== | | java.lang.Class | jclass | | java.lang.String | jstring | | arrays | jarray | | objecy[] | jobjectArray | | boolean[] | jbooleanArray | | byte[] | jbyteArray | | char[] | jcharArray | | short[] | jshortArray | | int[] | jintArray | | long[] | jlongArray | | float[] | jfloatArray | | double[] | jdoubleArray | | java.lang.Throwable | jthrowable | ## Library 庫 * 我們會把**常用的函數功能打包封裝為一個庫提供**給他人使用,Java 開發可以把封裝為 jar 包,Android 平台是 aar 包 :::info **Java 環境**:`.class` ->`.jar` 文件 **Android 環境**:`.class` -> `.jar` -> `.aar` 文件 **C/C++**:`.a` / `.so` 文件 ::: | 平台 | 特性 | 文件副檔名 | | -------- | -------- | -------- | | Linux | 動態庫 | .so | | Linux | 原代碼的二進制文件 | .o | | Linux | 多個.o 的集合 | .a | | Window | 動態庫 | .dll | | Window | 原代碼的二進制文件 | .obj | | Window | 多個.obj 的集合 | .lib | ### Android JNI 取名方式 * 當我們調用 Native 函數時,系統是如何找到對應的函數呢? 這些函數在 JNI 檔案種,而這 **JNI 檔案的取名有一定的習慣** (**==並非絕對==**) ```shell= # 假設 Java 包名為 com.android.internal.HelloWorld.java # 那麼在 JNI 的文件就是 com_android_internal_HelloWorld.cpp ``` ## 本地訪問 Java 上面有說明 Java 可以訪問本地代碼,**相對的 Native 也可以主動透過 JVM 訪問 Java 代碼** ### JNIEnv * JNIEnv 類型的變量,它是本地代碼函數中的第一個參數,**JNI interface pointer**,其本質是指向了 一個函數列表的指針 (**雙重、二級指針**),使用指針會使代碼更靈活 > ![](https://i.imgur.com/gKslAOn.png) * **==JNIEnv== 這個變量,在 C 語言中是 `JNINativeInterface_` 的++指針++**,所以 JNIEnv 就代表了指針,**它++描述了對 Java 層面的操作++** ```c= // C 語言中的定義 // 看成指針的別名 // typedef (const struct JNINativeInterface_ *) JNIEnv; typedef const struct JNINativeInterface_ *JNIEnv; ``` ***++C調用++*** ```c= // C file #include "jni.h" JNIEXPORT jstring JNICALL Java_testMyJava_jniTest_HelloWorld (JNIEnv *env, jobject o) { // env 是雙重指標 return (*env)->NewStringUTF(env, "Hello Alien, This is Static Register Function"); } ``` * C++ 則是對這個 `JNINativeInterface_` 封裝一個接口 (單指針) ```cpp= typedef JNIEnv_ JNIEnv; ``` ***C++調用*** ```cpp= // Cpp file #include "jni.h" JNIEXPORT jstring JNICALL Java_testMyJava_jniTest_HelloWorld (JNIEnv *env, jobject o) { // env is 單指標 return env->NewStringUTF("Hello Alien, This is Static Register Function"); } ``` ### jobject * **==jobject==,Java 中每個函數都包含一個 this,但是 ==JNI 中並沒有 this==,而 jobject 是對應 Java 中的所有對象,++jobject 代表物件++,而 ++JNI 引數的 jobject 是呼叫 native 方法的 this 對象++** :::info **static 函數內並不持有 this,所以該函數引數會改變為 jclass,++jclass 代表類++** ::: * 如何去使用 jobject 引數很重要 ```java= // Java 程式 class B { public native void testMethod_2(); } class A { public native void testMethod(); public static native void testStaticMethod(); public static void main(String... args) { A a = new A(); a.testMethod(); // 這時內部 jobject 就是 A 對象 B b = new B(); b.testMethod_2(); // JNI 內部 jobject 是 B 對象 } } // JNI.h 程式 JNIEXPORT void JNICALL Java_jniTest_testMethod(JNIEnv *, jobject); JNIEXPORT void JNICALL Java_jniTest_testStaticMethod(JNIEnv *, jclass); ``` ## JNIEnv 常用函數列表 ### Field * 訪問成員必須先,^1^ 取得成員的 Field ID,^2^ 再取得成員數值,這種用法就很像是 Java 的反射使用方式 | 函數 | 引數 | 功能 | | -------- | -------- | -------- | | GetFieldID : jfieldID | (JNIEnv \*env, jclass clazz, const char \*name, const char \*sig ) | 訪問物件的成員 Field (**非靜態**),成功返回 field ID, 否則返回 null | | Get<type\>Field : NativeType | (JNIEnv \*env, jobject obj, jfieldID fieldID) | 當獲取到 FieldID 後,就可以取得真正的數值 | | Set<type\>Field : void | (JNIEnv \*env, , jobject obj, jfieldID fieldID, NativeType value) | 同上,依照 jfieldID 設定數值 | * Get<type\>Field、Set<type\>Field,當中的 Get 代表了不同數據類型 ```java= GetObjectField(); GetBooleanField(); GetByteField(); GetCharField(); GetShortField(); ``` ### Methods * 訪問函數必須先,^1^ 取得成員的 Methid ID,^2^ 再呼叫函數,這種用法就很像是 Java 的反射使用方式 | 函數 | 引數 | 功能 | | -------- | -------- | -------- | | GetMethodID : jmethodID| (JNIEnv \*env, jclass clazz, const char \*name, const char \*sig ) | 取得物件 MethodID (非靜態) | | Call<type\>Method : NativeType | (JNIEnv \*env, jobject obj, jmethodID jmehtod, ...) | 透過 jMethodID 呼叫方法 | | Call<type\>MethodA : NativeType | (JNIEnv \*env, jobject obj, jmethodID jmehtod, jvalue \*args) | 以 **數組的方式** 傳遞參數 | | Call<type\>MethodV : NativeType | (JNIEnv \*env, jobject obj, jmethodID jmehtod, va_list args) | 以 **va_list 的方式** 傳遞參數 | ## JNI 註冊 ### 靜態註冊 - dll 動態庫 1. 使用 eclipse 創建一個新專案 testMyJava,並創建一個 jniTest.java 文件 > ![](https://i.imgur.com/tGnpLZS.png) 2. 在 jniTest.java 中寫入 native 函數 ```java= package testMyJava; public class jniTest { public native void HelloWorld(); public static void main(String[] args) { } } ``` 3. 使用 java 指令生成 `.class` & `.h` 文件 > 在環境變數中加入 java/bin 目錄 > > ![](https://i.imgur.com/5JzsHif.png) :::info * 如果 JDK 版本在 10 以上就必須使用 javac -h 因為 javah 指令已被整合進去 > 指令 : javac -h . jniTest.java ![](https://i.imgur.com/SGVIbff.png) ::: 4. 上面指令會生成 **`testMyJava_jniTest.h` 文件**,內部就是 JNI 翻譯語言,**內部註釋有說明 ++Function 簽名檔++,該簽名檔 ==生成 native 對應的函數宣告== (也就是 `public native void HelloWorld();` )** ```cpp= /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class testMyJava_jniTest */ #ifndef _Included_testMyJava_jniTest #define _Included_testMyJava_jniTest #ifdef __cplusplus extern "C" { #endif /* * Class: testMyJava_jniTest * Method: HelloWorld * Signature: ()V // Function 簽名檔 */ JNIEXPORT void JNICALL Java_testMyJava_jniTest_HelloWorld (JNIEnv *, jobject); // 生成 native 對應的函數 #ifdef __cplusplus } #endif #endif ``` 5. 我們可以自己去實現靜態本地語言,使用 CLion 編譯,將剛剛產生的 **`testMyJava_jniTest.h`、`jni.h`、`jni_md.h` 複製到 CLion 檔案中,並且在 CMakeList.txt 當中 add_library() ==編譯成動態庫 SHARED==** > ![](https://i.imgur.com/Ep8kTBU.png) :::info * `jni.h` 在 Java 目錄下的 include,`jni_md.h` 在 `include/win32` 目錄下 > ![](https://i.imgur.com/J4Jyk05.png) ::: 6. #include `testMyJava_jniTest.h` 檔案後編寫程式內容,最後**重新 build 一次 project,會在工程目錄 `/cmake-build-debug-mingw` 中產生 ++libfristLib.dll++ 檔案** (因在 window 中所以是 dell,而 Android 是基於 Linux 的所以會生成 so 檔案) > ![](https://i.imgur.com/MX7xWG3.png) 7. 在Java 程式中使用 `System.load()`,來讀取 dell 檔案 ```java= package testMyJava; public class jniTest { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libfirstLib.dll"); } public native String HelloWorld(); public static void main(String[] args) { jniTest t = new jniTest(); String str = t.HelloWorld(); System.out.println("native Func: " + str); } } ``` :::info 這邊要注意,**生成的動態庫要與系統++相同位元++,否則無法開啟**,64位元的電腦,下載 64 位元的 minMG64 > ![](https://i.imgur.com/G9EYeJg.png) ::: **--實作--** > ![](https://i.imgur.com/smQIzca.png) ### 動態註冊 1. 需要動態註冊方法進 static Array:該 **Array 型態為 JNINativeMethod,==注意每個成員代表的意義==,這裡會把 ++Java 層 & C 層的函數做映射++** (Key/Value) ```cpp= //型態 typedef struct { char *name; // Java 層 Function Name char *signature; // Java 層 簽名檔 void *fnPtr; // C 層 function 指標 } JNINativeMethod; #include "jni.h" void dynamicNative() { printf("This is Dynamic Function 1\n"); } //註冊 static const JNINativeMethod mMethods[] = { // Java 層函數名,Java 層函數簽名,Native 對應的指標 {"dynamicNative", "()V", (void *)dynamicNative}, }; ``` :::info JNINativeMethod 內的第二個成員,Function 簽名檔,**先關注引數,再關注回傳** ::: 2. **實現 `onLoad` 方法(JNI_OnLoad)**,**onLoad 方法是系統自動調用** ```cpp= static const char * mClassName = "testMyJava/DyJniTest"; //傳入的 class 路徑為全類名 (沒有副檔名) JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv * env = NULL; //雙重指標,JNIEnv 本身就是指標 // 獲取 evn,坑 : 第二個引數是 &env int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4); if(r != JNI_OK) { goto Failed; } else { jclass clz = (*env)->FindClass(env, mClassName); jint size = sizeof(mMethods) / sizeof(JNINativeMethod); r = (*env)->RegisterNatives(env, clz, mMethods, size); //配對 class & 函數名 if(r != JNI_OK) { goto Failed; } else { goto Success; } } Success: printf("JNI OnLoad Method Success\n"); return JNI_VERSION_1_4; Failed: printf("JNI OnLoad Method Fail\n"); return -1; } ``` 3. 在 Java 層調用 native Function ```java= package testMyJava; public class DyJniTest { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libfirstDyLib.dll"); } public native void DynamicFunc(); public static void main(String[] args) { DyJniTest d = new DyJniTest(); d.DynamicFunc(); } } ``` **--實作--** > ![reference link](https://i.imgur.com/NHRamdb.png) ### load & loadLibrary * System.load() 載入的是 **絕對路徑** * System.loadLibrary() 載入的是 **相對路徑**,Android 中使用 ## 應用 ### JNI 字符串修改 * 如果是**靜態註冊,==Function name 取名有++固定格式++==,否則會報 ++Exception in thread "main" java.lang.UnsatisfiedLinkError++ 錯誤** :::info **Java_ + 包名 + 類名 + Function name** ::: ```cpp= // JNI 層 /** * 宣告 */ #include "jni.h" JNIEXPORT jstring JNICALL Java_testMyJava_ToDoJni_fixFromJNI (JNIEnv *env, jobject o); /** * 定義 */ #include "MyHandleJNI.h" #include "string.h" JNIEXPORT jstring JNICALL Java_testMyJava_ToDoJni_fixFromJNI (JNIEnv *env, jclass c, jstring str) { jboolean isSuccess = 0; // Java String 轉為 C char* const char* getStr = (*env)->GetStringUTFChars(env, str, &isSuccess); printf("Is Copy Success: %d\n", isSuccess); if(getStr == NULL) { return NULL; } else { printf("getStr: %s\n", getStr); } char dirStr[128] = {0}; const char * newWorld = "Hi~ I'm JNI, "; strcpy(dirStr, newWorld); strcpy(dirStr + strlen(newWorld), getStr); printf("dirStr: %s\n", dirStr); //返回 Java string return (*env)->NewStringUTF(env, dirStr); } ``` * **Java 內部使用 utf-16 / 16 bit 的編碼,Jni 使用 utf-8 / unicode 編碼,GetStringUTFChars 是轉為 utf-8 char** ```java= // Java 層 package testMyJava; public class ToDoJni { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } // 使用靜態註冊 public static native String fixFromJNI(String str); public static void main(String[] args) { String str = fixFromJNI("Alien"); System.out.print("In Java: " + str + "\n"); } } ``` **--實作--** > ![](https://i.imgur.com/wibQ0cO.png) ### JNI 訪問 Java 類成員 * **主要有分為++靜態++、++動態++**,其訪問過程順序如同 **[Java 反射](https://hackmd.io/cuMlqQZ4QcujENqlkA1Zrw#參數-Field)** :::info 1. 獲取 class 2. 獲取字段 field 3. 獲取變數類型 int...等等 4. 以 new value 改變該字段 ::: | JNI 方法 | 功能 | | -------- | -------- | | GetObjectClass | **透過 jobject 取得對象的 class** | | FindClass | **透過 class path 取得 class** | | GetFieldID | 獲取 Java 字段 | | GetStaticFieldID | 獲取 Java static 字段 | ```java= // java 程式 public class ToDoJni { public int property = 30; public static double staticProperty = 90.9; static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } public native int fixParameter(); // 使用動態註冊 public static native double fixStaticParameter(); public static native String fixFromJNI(String str); public static void main(String[] args) { ToDoJni t = new ToDoJni(); int result = t.fixParameter(); System.out.println("fixParameter: " + result); double result2 = fixStaticParameter(); System.out.println("fixStaticParameter: " + result2); } } ``` * JNI 方面使用 **動態註冊**,要特別注意函數的 struct,如果函數簽名錯誤 JNI_onLoad 也會失敗 ```c= // JNI static const char * mClassName = "testMyJava/ToDoJni"; static int fixJavaParameter(JNIEnv *env, jobject obj) { // 同反射,先獲取 class jclass mClz = (*env)->GetObjectClass(env, obj); // Param3 : params name, Param4 : 簽名 jfieldID mFid = (*env)->GetFieldID(env, mClz, "property", "I"); jint i = (*env)->GetIntField(env, obj, mFid); // get 字段 printf("Get non-static parameter : %d\n", i); int newValue = i + 2000; (*env)->SetIntField(env, obj, mFid, newValue); return newValue; } static double fixJavaStaticParameter(JNIEnv *env, jclass clz) { // 同反射順序 // jclass mClz = (*env)->FindClass(env, mClassName); // 靜態類內部本來就有 class,所以不用特別取得 class jfieldID mFid = (*env)->GetStaticFieldID(env, clz, "staticProperty", "D"); jdouble jd = (*env)->GetStaticDoubleField(env, clz, mFid); printf("Get non-static parameter : %f\n", jd); double newValue = jd + 13.3; (*env)->SetStaticDoubleField(env, clz, mFid, newValue); return newValue; } static const JNINativeMethod mMethods[] = { // Java 層函數名,Java 層函數簽名,Native 對應的指標 {"fixParameter", "()I", (int *)fixJavaParameter}, {"fixStaticParameter", "()D", (double *)fixJavaStaticParameter}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv * env = NULL; //雙重指標 // 獲取 evn int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4); if(r != JNI_OK) { goto Failed; } else { jclass clz = (*env)->FindClass(env, mClassName); // 自動計算 Method size jint size = sizeof(mMethods) / sizeof(JNINativeMethod); r = (*env)->RegisterNatives(env, clz, mMethods, size); if(r != JNI_OK) { goto Failed; } else { goto Success; } } Success: printf("JNI OnLoad Method Success\n"); return JNI_VERSION_1_4; Failed: printf("JNI OnLoad Method Fail\n"); return -1; } ``` **--實作--** > 會注意到輸出 console 的時候並沒有按照順序輸出,原因是輸出的線程 & 處理的線程是分開的,而 C 的處理線程先處理完所以輸出 > > ![](https://i.imgur.com/BaZhwxs.png) ## JNI 調用 Java 方法 * 取得方法有分為 **++建構子++、++靜態++、++非靜態++** * 以下有常用方法,**主要分為靜態、非普通** | JNI 方法 | 功能 | | -------- | -------- | | GetFieldID | 獲取字段 | | GetStaticFieldID | 獲取靜態字段 | | Get \ SetXXXField | 設定字段,XXX 為類型 | | Get \ SetStaticXXXField | 設定靜態字段,XXX 為類型 | | GetMethodID | 獲取 Java 方法 | | GetStaticMethodID | 獲取 Java 靜態方法 | | **NewObject** | **創建新物件,並呼叫建構函數** | | CallVoidMethod | 呼叫方法 | | CallStaticVoidMethod | 呼叫靜態方法 | ### 靜態參數 * 獲取的方式如同 Java 反射,**在 JNI 中並沒有分私有、共有** :::info 1. 取得 class,FindClass 2. 取得字段 field,GetStaticFieldID 3. 獲取類型 char、short、int...,GetStaticXXXField 4. 設定參數 SetStaticXXXField,SetStaticXXXField ::: ```java= package testMyJava; public class ToDoJni { public static double staticProperty = 90.9; static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } public static native double fixStaticParameter(); public static void main(String[] args) { classInstance(); } } ``` * JNI 使用動態註冊 ```cpp= static double fixJavaStaticParameter(JNIEnv *env, jclass clz) { // 同反射順序 jclass mClz = (*env)->FindClass(env, mClassName); jfieldID mFid = (*env)->GetStaticFieldID(env, clz, "staticProperty", "D"); jdouble jd = (*env)->GetStaticDoubleField(env, clz, mFid); printf("Get non-static parameter : %f\n", jd); double newValue = jd + 13.3; (*env)->SetStaticDoubleField(env, clz, mFid, newValue); return newValue; } static const JNINativeMethod mMethods[] = { // Java 層 native 函數名,Java 層函數簽名,Native 對應的指標 {"fixStaticParameter", "()D", (double *)fixJavaStaticParameter}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv * env = NULL; //雙重指標 // 獲取 evn int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4); if(r != JNI_OK) { goto Failed; } else { jclass clz = (*env)->FindClass(env, mClassName); jint size = sizeof(mMethods) / sizeof(JNINativeMethod); r = (*env)->RegisterNatives(env, clz, mMethods, size); if(r != JNI_OK) { goto Failed; } else { goto Success; } } Success: printf("JNI OnLoad Method Success\n"); return JNI_VERSION_1_4; Failed: printf("JNI OnLoad Method Fail\n"); return -1; } ``` **--實作--** > ![](https://i.imgur.com/g5sxs7G.png) ### 靜態方法 * 調用靜態 static 方法,其使用方式如同 Java 在使用反射 :::info 1. 取得 class,FindClass 2. 取得函數,GetStaticMethodID 3. 呼叫 function,CallStaticVoidMethod ::: ```java= // Java 層 package testMyJava; public class ToDoJni { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } public static native void callJavaStaticMethod(); private static void JavaStaticMethod() { System.out.println("I'm Static Method in Java"); } public static void main(String[] args) { ToDoJni.callJavaStaticMethod(); } } ``` * JNI 使用++動態註冊++方法 ```cpp= static void callJavaStaticMethod(JNIEnv *env, jclass clz) { jmethodID mMethod = (*env)->GetStaticMethodID(env, clz, "JavaStaticMethod", "(Ljava/lang/String;)V"); jstring str = (*env)->NewStringUTF(env, "Alien"); (*env)->CallStaticVoidMethod(env, clz, mMethod, str); } static const JNINativeMethod mMethods[] = { // Java 層 native 函數名,Java 層函數簽名,Native 對應的指標 {"fixParameter", "()I", (int *)fixJavaParameter}, {"fixStaticParameter", "()D", (double *)fixJavaStaticParameter}, {"callJavaStaticMethod", "()V", (void *)callJavaStaticMethod}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv * env = NULL; //雙重指標 // 獲取 evn int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4); if(r != JNI_OK) { goto Failed; } else { jclass clz = (*env)->FindClass(env, mClassName); jint size = sizeof(mMethods) / sizeof(JNINativeMethod); r = (*env)->RegisterNatives(env, clz, mMethods, size); if(r != JNI_OK) { goto Failed; } else { goto Success; } } Success: printf("JNI OnLoad Method Success\n"); return JNI_VERSION_1_4; Failed: printf("JNI OnLoad Method Fail\n"); return -1; } ``` **--實作--** > ![](https://i.imgur.com/E3szr72.png) ### 構造函數 * 建立構造函數首先要先尋找方法,(<JNIEvn\>, <Class\>, <init\>, <簽名\>),在**尋找建構函數時第三個引數原本是`函數名稱`,要修改==函數名稱固定為 <init\> (字節碼分析)==,構造函數沒有返回,所以 ==++簽名返回固定為 V++==** (引數簽名不固定) > ASM 套件分析後,建構函數名稱就是就是 <init\> > > ![](https://i.imgur.com/CLfuYLK.png) :::info 1. 使用 FindClass 找尋類 2. 找尋方法,建構函數為名稱固定為 <init\>,GetMethodID 3. 建立物件,並呼叫建構函數,NewObject ::: ```java= // Java 層的新 class public class JNIObjectClass { public JNIObjectClass(int i) { System.out.println("I'm construct of JNIObjectJNI class, JNI set value : " + i); } } // Java 層使用 package testMyJava; public class ToDoJni { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } public static native void classInstance(); public static void main(String[] args) { classInstance(); } } ``` * JNI 使用動態註冊 ```c= int a = 0; static void makeJavaClassInstance(JNIEnv *env, jclass jClz) { static int a = 0; //"1. " // jclass 必須在找一次,引數的 jclass jClz 不一定是需要的類 jclass realClz = (*env)->FindClass(env, "testMyJava/JNIObjectClass"); if(realClz == NULL) { printf("Find Class Fail"); return; } // Step 1. 找 construct 方法 jmethodID method = (*env)->GetMethodID(env, realClz, "<init>", "(I)V"); // Step 2. 建立物件、呼叫建構函數 jobject obj = (*env)->NewObject(env, realClz, method); //"2. " // 坑...不需要再呼叫一次 Step 3. 呼叫 construct 方法 jint value = 10; //(*env)->CallStaticVoidMethod(); 靜態方法使用,建構函數並不是靜態方法 //(*env)->CallVoidMethod(env, obj, method, value); printf("JNI print : Make Class form JNI, time = %d", ++a); } static const JNINativeMethod mMethods[] = { // Java 層 native 函數名,Java 層函數簽名,Native 對應的指標 {"classInstance", "()V", (void *)makeJavaClassInstance}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv * env = NULL; //雙重指標 // 獲取 evn int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4); if(r != JNI_OK) { goto Failed; } else { jclass clz = (*env)->FindClass(env, mClassName); jint size = sizeof(mMethods) / sizeof(JNINativeMethod); r = (*env)->RegisterNatives(env, clz, mMethods, size); if(r != JNI_OK) { goto Failed; } else { goto Success; } } Success: printf("JNI OnLoad Method Success\n"); return JNI_VERSION_1_4; Failed: printf("JNI OnLoad Method Fail\n"); return -1; } ``` 1. 在這邊可以看到明明已經有 jclass 引數,卻仍然需要在找一次,因為引數的 jclass,是呼叫 native 函數的 jclass,然而 **==++呼叫的地方並不一定是需要載入的 class++==,所以一定要用 FindClass(路徑) 再次尋找** 2. **NewObject 方法就會呼叫 Class construct**,不需要再透過 CallVoidMehtid 方法再次呼叫 **--實做--** > ![](https://i.imgur.com/KAzGtKP.png) ### 非靜態方法 * 非靜態方法都需要一個類,所以**必須先找到類,使用建構函數建立出物件,再呼叫該方法** ```java= // 要呼叫的 class public class JNIObjectClass { private int i = 0; public JNIObjectClass(int i) { this.i = i; //System.out.println("I'm construct of JNIObjectJNI class, JNI set value : " + i); } public void HeyGuy(String str) { System.out.println(str + ", i = " + i); } } // 使用 native package testMyJava; public class ToDoJni { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } public static native String ShowValue(); public static void main(String[] args) { System.out.println(ShowValue()); } } ``` * JNI 使用動態註冊 ```c= static jstring callJavaMethod(JNIEnv *env, jobject jObj) { // 建立物件 jclass realClz = (*env)->FindClass(env, "testMyJava/JNIObjectClass"); if(realClz == NULL) { printf("Cannot Find Class"); return 0; } jmethodID construct = (*env)->GetMethodID(env, realClz, "<init>", "(I)V"); jobject makeObj = (*env)->NewObject(env, realClz, construct); (*env)->CallVoidMethod(env, makeObj, construct, 3000); // 呼叫物件方法 jfieldID field = (*env)->GetFieldID(env, realClz, "i", "I"); jint i = (*env)->GetIntField(env, makeObj, field); i += 210; (*env)->SetIntField(env, makeObj, field, i); jmethodID method = (*env)->GetMethodID(env, realClz, "HeyGuy", "(Ljava/lang/String;)V"); jstring str = (*env)->NewStringUTF(env, "JNI Process finish"); (*env)->CallVoidMethod(env, makeObj, method, str); return (*env)->NewStringUTF(env, "Hi, I'm JNI ~"); } static const JNINativeMethod mMethods[] = { // Java 層 native 函數名,Java 層函數簽名,Native 對應的指標 {"ShowValue", "()Ljava/lang/String;", (jstring *)callJavaMethod} }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv * env = NULL; //雙重指標 // 獲取 evn int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4); if(r != JNI_OK) { goto Failed; } else { jclass clz = (*env)->FindClass(env, mClassName); jint size = sizeof(mMethods) / sizeof(JNINativeMethod); r = (*env)->RegisterNatives(env, clz, mMethods, size); if(r != JNI_OK) { goto Failed; } else { goto Success; } } Success: printf("JNI OnLoad Method Success\n"); return JNI_VERSION_1_4; Failed: printf("JNI OnLoad Method Fail\n"); return -1; } ``` **--實作--** > ![](https://i.imgur.com/P9qRurY.png) ## JNI 引用 該**引用的關係有點像是 Java ++強引用++、++軟引用++、++弱引用++、++虛引用++ 在 [JVM 中 GC 關係](https://hackmd.io/KPXIegnbSA-6bFYE6cIMGw#Java-引用)**,以下會分開介紹 **JNI 引用與 JVM 的關係** * 返回的類型是實體類 (jobject...) 才是引用,**引用是物件導向才有的,如同 C++ `&` Reference**,如果返回的是指針則不是 reference,**==只有引用才需要釋放==**,可以參考野指針的問題 ```cpp= struct _jobject; typedef struct _jobject *jobject; typedef jobject jclass; typedef jobject jthrowable; typedef jobject jstring; typedef jobject jarray; typedef ... // 以下不是 struct _jfieldID; typedef struct _jfieldID *jfieldID; struct _jmethodID; typedef struct _jmethodID *jmethodID; ``` ### 局部引用 * **透過 Function 內部,NewObject 創建,生命週期如同 Function** * 在局部引用中,代表的意思就是 Function 中配置的 JNIEnv 對象,而 **JNI 中的 JNIEnv 是由 JVM 來管控**,**當函式執行結束時就會自動釋放** * **以 JVM 中的++可達性分析++來說**,**如果局部引用仍被其它的全域對象引用時,就無法被釋放,這時就 ==可以使用 ++DeleteLocalRef 手動釋放它++==** ```cpp= // 原型 typedef jobject jstring; jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf); // JNI code static void HeyLocal(JNIEnv *env, jobject obj) { jclass clz = (*env)->FindClass(env, "java/lang/String"); jmethodID method = (*env)->GetMethodID(env, clz, "<init>", "([C)V"); jcharArray charArr = (*env)->NewCharArray(env, len); jstring str = (*env)->NewObject(env, clz, method, charArr); (*env)->DeleteLocalRef(env, str); } ``` > 可以觀察到它的**原型是 reference,所以可以釋放引用** ### 全局引用 * **使用 NewGlobalRef 創建全局物件,生命週期如整個程式** * 在**全局中引用,無法自動釋放,它與 Java 的強引用不同 (會依照可達性分析回收),==全局引用不受 JVM 管理==,不受可達性分析**,回收必須自己**手動++透過 DeleteGlobalRef 手動回收++**,所以必須小心使用 ```cpp= // 原型 typedef jobject jstring; jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf); // JNI code jstring str = (*env)->NewStringUTF(env, "123 站著穿"); static void HeyGlobal(JNIEnv *env, jobject obj) { jclass clz = (*env)->FindClass(env, "java/lang/String"); str = (*env)->NewGlobalRef(env, clz); (*env)->DeleteGlobalRef(env, str); } ``` > **全局引用必須手動刪除**,因為它不歸 JVM 管理 ### 弱全局引用 * **透過 NewWeakGlobalRef 創建** * **弱全局引用,如同[Java 軟引用](https://hackmd.io/KPXIegnbSA-6bFYE6cIMGw?both#軟引用),在 JVM ++記憶體不足時++會觸發的回收**,但也可以++透過 DeleteWeakGlobalRef 手動回收++ ```cpp= // 原型 typedef jobject jstring; jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf); // JNI code static jstring str = (*env)->NewStringUTF(env, "123 站著穿"); static void HeyGlobal(JNIEnv *env, jobject obj) { jclass clz = (*env)->FindClass(env, "java/lang/String"); str = (*env)->NewWeakGlobalRef(env, clz); (*env)->DeleteLocalRef(env, str); } ``` ### 野指針 & 懸空指針 * 指針存有內存地址,**該地址內卻沒有應該存放的資料 (已被釋放),這時該指針就稱為野指針** * 宣告出指標變量,但是並沒有賦予它值,**它可能隨意指向任何地方,該指針稱為懸空指針** * 會使用 [**static 靜態期間、無連結**](https://hackmd.io/uf-tHfltRVCV45D1h6ZiGA?view#靜態期間、無連結) 的指標來演示 * **==局部引用才能刪除==,非局部引用不能刪除,否則會異常** ```java= public class ToDoJni { static { System.load("C:\\Users\\Admin\\CLionProjects\\untitled1\\cmake-build-debug-mingw\\libtoDoLib.dll"); } public native String makeString(int i); public static void main(String[] args) { ToDoJni t = new ToDoJni(); String str = t.makeString(10); System.out.println("Test String from JNI Size : " + str.length() + "\n"); String str2 = t.makeString(10); System.out.println("Test String from JNI Size : " + str2.length() + "\n"); } } ``` * 使用 JNI 動態註冊 ```c= static jstring newString(JNIEnv *env, jobject obj, jint len) { static jclass clz = NULL; static jmethodID method = NULL; if(clz == NULL) { clz = (*env)->FindClass(env, "java/lang/String"); if(clz == NULL) { printf("Cannot find Class :("); return NULL; } else { printf("Find Class :D\n"); } } if(method == NULL) { method = (*env)->GetMethodID(env, clz, "<init>", "([C)V"); if(method == NULL) { printf("Cannot Find Method :("); return NULL; } else { printf("Find Method :D\n"); } } jcharArray array = (*env)->NewCharArray(env, len); printf("Info: Class = %p, Method = %p, Array = %p\n", clz, method, array); jstring str = (*env)->NewObject(env, clz, method, array); //"1." (*env)->DeleteLocalRef(env, clz); //"2. " clz = NULL; //"3. " //(*env)->DeleteLocalRef(env, method); method = NULL; printf("End Of Function :D\n"); return str; } static const JNINativeMethod mMethods[] = { // Java 層 native 函數名,Java 層函數簽名,Native 對應的指標 {"makeString", "(I)Ljava/lang/String;", (jstring *)newString}, }; ``` 1. 使用完後手動刪除 local ref,避免被引用 2. 必須指向 NULL,因為 **++變數是靜態區域變數,會存在靜態區塊記憶體++** 3. 刪除非 ref 會出錯,會發出以下警告,目前刪除的是指針 ```typescript= warning: passing argument 2 of '(*env)->DeleteLocalRef' from incompatible pointer type [-Wincompatible-pointer-types] (*env)->DeleteLocalRef(env, method); ^~~~~~ note: expected 'jobject' {aka 'struct _jobject *'} but argument is of type 'jmethodID' {aka 'struct _jmethodID *'} ``` > 如果上面不手動調用 reference 刪除也可以,不過手動刪除是好習慣 **--實作--** > ![](https://i.imgur.com/Hv19IMf.png) > ## Appendix & FAQ :::info ::: ###### tags: `Android 系統` `Android NDK` `Android 進階`