---
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++)
> 
### JNI 使用時機
1. 應用程序需要一些平台相關的 feature 支持,像是 藍芽、WIFI ...
2. 兼容使用其他語言寫的 Lib,**程式實現復用**
3. 快速調用,越接近底層的語言(C、C++、彙編) 程式運行的速度越塊
### Android JNI
* 簡單創建 C/C++ 的專案,是最快開啟 JNI 使用大門的方法
> New -> New Project -> Native C/C++
>
> 
### 交叉編譯
* 在不同平台上 (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` 檔案
>
> 
### 基礎類型的映射
* 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**,其本質是指向了 一個函數列表的指針 (**雙重、二級指針**),使用指針會使代碼更靈活
> 
* **==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 文件
> 
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 目錄
>
> 
:::info
* 如果 JDK 版本在 10 以上就必須使用 javac -h 因為 javah 指令已被整合進去
> 指令 : javac -h . jniTest.java

:::
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==**
> 
:::info
* `jni.h` 在 Java 目錄下的 include,`jni_md.h` 在 `include/win32` 目錄下
> 
:::
6. #include `testMyJava_jniTest.h` 檔案後編寫程式內容,最後**重新 build 一次 project,會在工程目錄 `/cmake-build-debug-mingw` 中產生 ++libfristLib.dll++ 檔案** (因在 window 中所以是 dell,而 Android 是基於 Linux 的所以會生成 so 檔案)
> 
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
> 
:::
**--實作--**
> 
### 動態註冊
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();
}
}
```
**--實作--**
> 
### 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");
}
}
```
**--實作--**
> 
### 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 的處理線程先處理完所以輸出
>
> 
## 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;
}
```
**--實作--**
> 
### 靜態方法
* 調用靜態 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;
}
```
**--實作--**
> 
### 構造函數
* 建立構造函數首先要先尋找方法,(<JNIEvn\>, <Class\>, <init\>, <簽名\>),在**尋找建構函數時第三個引數原本是`函數名稱`,要修改==函數名稱固定為 <init\> (字節碼分析)==,構造函數沒有返回,所以 ==++簽名返回固定為 V++==** (引數簽名不固定)
> ASM 套件分析後,建構函數名稱就是就是 <init\>
>
> 
:::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 方法再次呼叫
**--實做--**
> 
### 非靜態方法
* 非靜態方法都需要一個類,所以**必須先找到類,使用建構函數建立出物件,再呼叫該方法**
```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;
}
```
**--實作--**
> 
## 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 刪除也可以,不過手動刪除是好習慣
**--實作--**
> 
>
## Appendix & FAQ
:::info
:::
###### tags: `Android 系統` `Android NDK` `Android 進階`