Android進階學習(10)-- JNI使用(Java 與 C/C++ 互相訪問、調用)

什麼是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 靜態註冊 和 動態註冊

靜態註冊

實現流程
  1. 編寫Java文件,定義native方法
  2. Java命令行編譯得到.class .h 文件,將.h文件複製到 C 的項目中
  3. 定義 .c 文件,實現 .h 文件中的方法,添加 jni.h 頭文件
  4. 編譯 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 靜態註冊這就實現了

動態註冊

實現流程
  1. 編寫Java文件,定義native 方法
  2. 在C項目中定義 .c 文件,對應實現Java中定義的native方法
  3. .c 文件中實現JNI_OnLoad 方法
  4. 編譯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語言中也存在三種引用:

  1. **全局引用:**調用NewGlobalRef基於局部引用創建,會阻GC回收所引用的對象。可以跨方法、跨線程使用。JVM不會自動釋放,
    必須調用DeleteGlobalRef手動釋放(*env)->DeleteGlobalRef(env,g_cls_string);
  2. **局部引用:**通過NewLocalRef和各種JNI接口創建(FindClass、NewObject、GetObjectClass和NewCharArray等),當函數執行完成後,函數內的局部引用生命週期也就結束了。
  3. ** 弱全局引用:**調用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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章