JNI 靜態註冊、動態註冊
什麼是JNI
Java Native Interface,Java調用本地方法的技術,簡單來說,當Java運行在Windows平臺時,通過JNI和Windows底層也可以理解爲和 C/C++ 進行交互。Jvm就是通過大量的JNI技術使得Java能夠在不同平臺上運行。
Java相關命令
javac xxx.java //生成 .class 文件
javah xxx.xxx(全類名) //生成 .h 頭文件
javac -h . xxx.java //Java1.8 以上 代替上面兩個命令 生成 .class .h 文件
javap -s -p xxx.class//查看類中的字段和方法的簽名
Java 方法、變量的簽名
Java類型 | 簽名 |
---|---|
byte | B |
short | S |
int | I |
long | J |
float | F |
double | D |
boolean | Z |
char | C |
void | V |
方法的簽名寫法:(參數簽名)返回值類型簽名
如 : 方法public int test(int i, String s, long[] l){ ... }
所對應的簽名就是(ILjava/lang/String;[J)I
,方法的簽名也可以通過命令行 javap -s -p xxx.class
去查看;
JNI 數據類型
基本類型
JNI類型 | Java類型 |
---|---|
jbyte | byte |
jshort | short |
jint | int |
jlong | long |
jfloat | float |
jdouble | double |
jboolean | boolean |
jchar | char |
void | void |
引用類型
JNI類型 | Java類型 |
---|---|
jclass | Class |
jobject | Object |
jstring | String |
jobejctArray | Object[] |
jbyteArray | byte[] |
jshortArray | short[] |
jintArray | int[] |
jlongArray | long[] |
jdoubleArray | double[] |
jbooleanArray | boolean[] |
jcharArray | char[] |
jthrowable | Throwable |
靜態庫 和 動態庫
靜態庫:這類庫的名字一般是 xxx.a ;利用靜態函數庫編譯的文件較大,整個函數庫所有的數據都會被整合進目標代碼中;優點,編譯後執行程序不需要外部的函數庫支持;缺點,如果靜態函數庫改變了,需要重新編譯。
動態庫:這類庫的名字一般是 xxx.so ;相比於靜態庫,在編譯時並沒有整合進目標代碼,在程序執行到相關函數時才調用對應函數庫的函數,因此生成的可執行文件較小。運行環境必須提供對應的庫,動態函數庫的改變不影響程序,動態庫升級比較方便。
JNI 靜態註冊 和 動態註冊
靜態註冊
實現流程
- 編寫Java文件,定義native方法
- Java命令行編譯得到.class .h 文件,將.h文件複製到 C 的項目中
- 定義 .c 文件,實現 .h 文件中的方法,添加 jni.h 頭文件
- 編譯 C項目 得到 .dll文件,回到Java中,加載 .dll 文件,實現JNI調用
具體實現
新建 StaticReg.java 文件
public class StaticReg {
// c/c++ 層要實現的方法
public native void Hello();
public static void main(String[] args) {
}
}
進入到StaticReg.java所在的目錄中,通過命令行生成 .class .h 文件:
javac -h . StaticReg.java
打開Clion,新建一個C++ Library項目
新建項目之後,將上一步生成的 .h 文件複製到 C 項目中,並且以同樣的文件名新建一個 .c 文件,實現裏面的函數
這兩個參數代表的含義:
JNIEnv* env參數:實質上代表Java 環境,通過這個指針,就可以對Java端代碼進行操作,創建Java類的對象,調用Java對象方法,獲取Java對象屬性等;
jobject obj參數:如果native 方法是 static,那麼這個 obj 就代表這個native的實例;如果native方法不是 static,那麼這個 obj 就代表native方法的類的class對象實例;
編寫完成之後,在CMakeLists.txt 中添加以下代碼:
## staticReg 要生成的動態庫文件名
## SHARED 庫的類型
## 後面的.c .h 文件 是指要包含的源文件
add_library(staticReg SHARED com_shy_sample_jniReg_StaticReg.c com_shy_sample_jniReg_StaticReg.h)
添加完成之後編譯項目
編譯完成後會在目錄下生成 這麼倆個文件, .dll 文件就是在Windows平臺上生成的動態庫,在Linux平臺與之對應的就是 .so 庫
回到Java代碼中,StaticReg.java中添加以下代碼:
public class StaticReg {
static {
//引入 C 編譯出來的 .dll 文件
System.load("E:\\CProject\\study_jni_reg\\cmake-build-debug\\libstaticReg.dll");
}
public native void Hello();
public static void main(String[] args) {
StaticReg reg = new StaticReg();
reg.Hello();
}
}
運行效果:
JNI 靜態註冊這就實現了
動態註冊
實現流程
- 編寫Java文件,定義native 方法
- 在C項目中定義 .c 文件,對應實現Java中定義的native方法
- .c 文件中實現JNI_OnLoad 方法
- 編譯C項目,得到.dll 文件,回到Java項目中加載 .dll文件,實現JNI調用
具體實現
首先,新建Java文件,DynamicReg.java
public class DynamicReg {
public native void sayHello();
public native void getRandom();
public static void main(String[] args) {
}
}
和靜態註冊不同的是,我們不再需要去編譯頭文件等,直接再C 項目中 新建 DynamicReg.c 文件,代碼中有詳細註釋:
#include "jni.h"
//這兩個方法 分別對應 Java中定義的兩個 native方法
void sayHello(JNIEnv *env, jobject jobj){
printf("JNI -> say Hello ! \n");
}
jint getRandom(JNIEnv *env, jobject jobj){
return 666;
}
// Java 類的 全類名
static const char * mClassName = "com/shy/sample/jniReg/DynamicReg";
//存放JNINativeMethod結構體的數組,
//結構體三個參數分別代表: java中native方法名, 方法簽名, C中對應的方法指針
static const JNINativeMethod mMethods[] = {
{"sayHello", "()V", (void*)sayHello},
{"getRandom", "()I",(void*)getRandom},
};
//JNI_OnLoad 方法 在Java 端調用System.load後會執行
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
printf("JNI_OnLoad start _______________\n");
JNIEnv* env = NULL;
//獲得 JniEnv
int r = (*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
jclass mainActivityCls = (*env)->FindClass(env, mClassName);
// 註冊 如果小於0則註冊失敗
// 一定要注意 RegisterNatives 最後一個參數,代表方法個數
r = (*env)->RegisterNatives(env,mainActivityCls,mMethods,2);
if(r != JNI_OK )
{
return -1;
}
printf("JNI_OnLoad end __________________\n");
return JNI_VERSION_1_4;
}
上述代碼中:
sayHello 和 getRandom 分別對應Java 代碼中定義的兩個native方法;
mClassName ,Java中的類的全類名;
mMethods,一個數組,存放的是 JNINativeMethod 結構體的元素,這個數組主要是匹配 C 和 Java 兩端的方法;
JNI_OnLoad 方法,當Java中執行System.load時,會執行這個方法,這個方法也是動態註冊的關鍵方法;
然後編譯項目,生成 .dll 和 .dll.a 文件:
回到Java 端,修改DynamicReg.java代碼:
public class DynamicReg {
static {
System.load("E:\\CProject\\study_jni_reg\\cmake-build-debug\\libdynamicReg.dll");
}
public native void sayHello();
public native int getRandom();
public static void main(String[] args) {
DynamicReg dynamicReg = new DynamicReg();
dynamicReg.sayHello();
System.out.println("返回結果: " + dynamicReg.getRandom());
}
}
運行結果:
動態註冊相比於靜態註冊,省去了我們手動編譯java文件,導入.h頭文件的過程,在JNI_OnLoad 方法中幫我們匹配了方法調用;
C/C++ 訪問 Java 中的變量
在上面的例子中,已經完成了Java 通過 JNI 調用 C/C++,很多時候我們在C/C++中也需要獲取Java類中的變量,對他們進行一系列操作,下面就來實現 C/C++ 中獲取 Java 類中的變量
新建一個 Test.java 文件
public class Test {
// 這個要在C 項目編譯後,生成 .dll 文件之後 再加載這個文件 我這裏提前寫上了
static {
System.load("E:\\CProject\\study_jni_reg\\cmake-build-debug\\libchangeNum.dll");
}
int num = 1;
static int staticNum = 100;
String name = "Sunhy";
public native void changeNum();
public native void changeStaticNum();
public native String sayHello(String str);
public static void main(String[] args) {
Test test = new Test();
test.changeNum();
test.changeStaticNum();
System.out.println("num = " + test.num);
System.out.println("staticNum = " + staticNum);
System.out.println("sayHello -> " + test.sayHello(test.name));
}
}
Test.java中,定義了普通變量、靜態變量、有返回值的native函數,下面具體來實現一下C/C++訪問普通變量、靜態變量以及返回給Java層返回值。
訪問普通變量
首先在C 項目中創建 ChangeNum.c 文件,導入頭文件#include "jni.h"
,並且對應實現Java中的方法,採用靜態註冊,所以方法名用 全類名+方法名 來對應
#include "jni.h"
#include <stdlib.h>
#include <string.h>
#include <windows.h>
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeNum
(JNIEnv* env, jobject jobj){
}
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeStaticNum
(JNIEnv* env, jobject jobj){
}
JNIEXPORT jstring JNICALL Java_com_shy_sample_jniField_Test_sayHello
(JNIEnv* env, jobject jobj, jstring str){
}
先編寫訪問普通變量的方法Java_com_shy_sample_jniField_Test_changeNum,獲取到Java類中的num變量,並且修改它:
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeNum
(JNIEnv* env, jobject jobj){
// 1.獲取類
jobject clz = (*env)->GetObjectClass(env, jobj);
// 2.獲取屬性的ID 最後一個參數是變量的簽名
jfieldID numId = (*env)->GetFieldID(env, clz, "num", "I");
// 3.獲取變量的值
jint num = (*env)->GetIntField(env, clz, numId);
printf("JNI -> C -> num = %d\n", num);
// 4.修改變量的值
(*env)->SetIntField(env, clz, numId, 1000 + num);
}
這就完成了對Java類中普通變量num的值的修改
訪問靜態變量
訪問靜態變量和訪問普通變量流程是一樣的,只不過每一步調用的方法不同,編寫Java_com_shy_sample_jniField_Test_changeStaticNum方法:
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_changeStaticNum
(JNIEnv* env, jobject jobj){
//獲取類的方法有兩種 FindClass 需要傳入類的全類名
//jobject clz = (*env)->FindClass(env, "com/shy/sample/jniField/Test");
jobject clz = (*env)->GetObjectClass(env, jobj);
jfieldID staticNumId = (*env)->GetStaticFieldID(env, clz, "staticNum", "I");
jint staticNum = (*env)->GetStaticIntField(env, clz, staticNumId);
printf("JNI -> C -> staticNum = %d\n", staticNum);
(*env)->SetStaticIntField(env, clz, staticNumId, 1000 + staticNum);
}
訪問靜態變量,調用的都是GetStaticXXX 或者 SetStaticXXX;
C/C++返回值給Java
前面的例子中,都是無返回值void類型的native函數,這裏通過實現Java類中的sayHello(String str),來實現接受Java傳遞的參數,並且返回值給Java:
JNIEXPORT jstring JNICALL Java_com_shy_sample_jniField_Test_sayHello
(JNIEnv* env, jobject jobj, jstring str){ //注意這裏,Java傳遞的參數這裏要對應
jboolean iscp;
// 1. 先獲取到 java 端傳過來的參數
const char* name = (*env) -> GetStringUTFChars(env, str, &iscp);
// 2. 定義一個字符數組
char buf[128] = {0};
// 3. 拼接字符數組
sprintf(buf, "Hello --->> %s", name);
// 4. 釋放資源
(*env) -> ReleaseStringUTFChars(env, str, name);
// 5. 返回
return (*env) -> NewStringUTF(env, buf);
}
編譯C 項目,生成 .dll 文件,運行Java代碼,運行結果:
這裏我們會發現,打印的日誌順序反了,應該 下面兩句 JNI 開頭的先打印,因爲他們在C 的方法中;這是因爲,C/C++ 和 Java 分別有自己的緩衝區,每次刷新緩衝區,C/C++才能將標準輸出送到Java的控制檯。
C/C++ 調用Java方法
C/C++ 可以訪問 Java中的變量,那麼肯定也能調用Java中的方法,這種場景經常用於,C/C++ 需要創造返回一個Java對象時使用,如需要返回一個Bitmap時,那麼就需要在C/C++ 層調用對應Java方法去實現。
C/C++ 調用Java方法,主要區分爲 調用構造方法、非靜態方法、靜態方法。首先,在Java端新建一個JNICall的類:
public class JNICall {
// 構造方法
public JNICall(){
System.out.println("JNICall -> Constructor is be invoked ");
}
// 普通方法
public void JNICallMethod(){
System.out.println("JNICall -> Method is be invoked ");
}
// 靜態方法
public static void JNICallStaticMethod(){
System.out.println("JNICall -> Static method is be invoked ");
}
}
接着,繼續使用上面例子中的Test.java,在其中定義三個native方法:
public class Test {
//。。。多餘代碼省略
//在C/C++端實現下面的三個方法,去調用JNICall.java中的方法
public native void callConstructor();
public native void callMethod();
public native void callStaticMethod();
public static void main(String[] args) {
//。。。多餘代碼省略
Test test = new Test();
test.callConstructor();
test.callMethod();
test.callStaticMethod();
}
}
在C 項目中實現定義的三個方法,爲了方便就直接寫在上面定義的ChangNum.c 中:
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callConstructor
(JNIEnv* env, jobject jobj){
};
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callMethod
(JNIEnv* env, jobject jobj){
};
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callStaticMethod
(JNIEnv* env, jobject jobj){
};
下面就來分別實現三個方法
調用構造方法
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callConstructor
(JNIEnv* env, jobject jobj){
// 1. 獲取到要調用的類
jclass clz = (*env) -> FindClass(env, "com/shy/sample/jniField/JNICall");
// 2. 獲取要調用的方法的ID 構造方法方法名必須傳入 <init>
jmethodID methodId = (*env) -> GetMethodID(env, clz, "<init>", "()V");
// 3. 創建 要調用類的 對象
jobject obj = (*env) -> NewObject(env, clz, methodId);
// 4. 調用
(*env) -> CallVoidMethod(env, obj, methodId);
};
調用構造方法,需要注意一點,方法名必須傳入 <init>
調用非靜態方法
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callMethod
(JNIEnv* env, jobject jobj){
// 1. 獲取到要調用的類
jclass clz = (*env) -> FindClass(env, "com/shy/sample/jniField/JNICall");
// 2. 獲取要調用的方法的ID
jmethodID methodId = (*env) -> GetMethodID(env, clz, "JNICallMethod", "()V");
// 3. 創建 要調用類的 對象
// 就如同java 中 new 對象一樣,需要指定構造方法
jmethodID constructorId = (*env) -> GetMethodID(env, clz, "<init>", "()V");
jobject obj = (*env) -> NewObject(env, clz, constructorId);
// 4. 調用
(*env) -> CallVoidMethod(env, obj, methodId);
};
調用普通方法,就和Java很像,需要知道調用哪個類,new出來它的對象,然後調用
調用靜態方法
JNIEXPORT void JNICALL Java_com_shy_sample_jniField_Test_callStaticMethod
(JNIEnv* env, jobject jobj){
// 1. 獲取到要調用的類
jclass clz = (*env) -> FindClass(env, "com/shy/sample/jniField/JNICall");
// 2. 獲取要調用的方法的ID
jmethodID methodId = (*env) -> GetStaticMethodID(env, clz, "JNICallStaticMethod", "()V");
// 3. 調用
(*env) -> CallStaticVoidMethod(env, clz, methodId);
};
調用靜態方法,也是和Java很像,在Java中靜態方法是通過 類名.方法名 去調用的,所以,調用靜態方法,就省去了new一個對象的操作。
野指針問題
上面的代碼中,雖然功能都實現了,但是都存在內存泄漏,溢出的風險。在Java中有四種引用,分別是強、軟、弱、虛引用,C語言中也存在三種引用:
- **全局引用:**調用NewGlobalRef基於局部引用創建,會阻GC回收所引用的對象。可以跨方法、跨線程使用。JVM不會自動釋放,
必須調用DeleteGlobalRef手動釋放(*env)->DeleteGlobalRef(env,g_cls_string);
- **局部引用:**通過NewLocalRef和各種JNI接口創建(FindClass、NewObject、GetObjectClass和NewCharArray等),當函數執行完成後,函數內的局部引用生命週期也就結束了。
- ** 弱全局引用:**調用NewWeakGlobalRef基於局部引用或全局引用創建,不會阻止GC回收所引用的對象,可以跨方法、跨線程使
用。引用不會自動釋放,在JVM認爲應該回收它的時候(比如內存緊張的時候)進行回收而被釋放。或調用
DeleteWeakGlobalRef手動釋放(*env)->DeleteWeakGlobalRef(env,g_cls_string)
這就會出現一種情況:
JNIEXPORT jstring JNICALL Java_newString
(JNIEnv * env, jobject jobj){
// 定義靜態的局部變量
static jclass cls_string = NULL;
if (cls_string == NULL) {
printf("cls_string is null \n");
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
.....
}
上述代碼中的 cls_string 是一個靜態的局部變量,那麼當方法執行一次後 靜態變量cls_string 會指向 FindClass方法返回的局部引用的首地址,當函數執行結束,局部引用會失效,但是cls_string 中存放的是地址,當第二次執行該函數時,cls_string 不爲NULL,也就不會執行 if 語句,從而導致它成爲一個野指針;
所以在編寫 JNI 時,一定要手動釋放,在上述代碼結束前把 cls_string 賦空值:
JNIEXPORT jstring JNICALL Java_newString
(JNIEnv * env, jobject jobj){
// 定義靜態的局部變量
static jclass cls_string = NULL;
if (cls_string == NULL) {
printf("cls_string is null \n");
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
.....
(*env)->DeleteLocalRef(env, cls_string);
cls_string = NULL;
}