NDK 基礎知識–JNI java與c++

NDK 基礎知識–JNI

開發環境: Android studio v3.6.1

(3.6.0都支持kotlin與c/c++互相調用,是該學學NDK了,不能再找理由了)

NDK 可以讓我們Android 應用中使用C、C++代碼。以前Android 都是使用java,NDK中包含JNI (java本地接口)可以使用java 調用c、c++等。如今kotlin被Android 官方宣佈第一開發語言。kotlin與java是100%兼容的(我認爲kotlin、java都依靠jvm,他們都要編譯成java字節碼,kotlin只是利用它的編譯器特性,簡化了java語法。這應該就是以後編程語言發展趨勢吧,讓我們少做點,電腦多做的)

好了,廢話不說了,正片開始

1.native方法

在java 文件中聲明一個native方法

//Test.java
package com.wkk.ndkdemo;
public class Test {
    //聲明native方法,不用實現,方法實現代碼在c或c++中
    native void test();
}

如果是kotlin 則是external關鍵詞

  external fun test()

c++ 文件

#include <jni.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_Test_test(JNIEnv *env, jobject thiz) {

}

這裏c++的方法名特麼長Java_com_wkk_ndkdemo_Test_test 這個名字是有固定語法的

在這裏插入圖片描述

方法的第一個參數是JNIEnv指針, JNIEnv 是一個結構體,定義了許多與java的方法。

方法的第二個參數jobject表示調用test()方法的對象。test()方法是成員方法,如果是個靜態方法,則第二個參數就是jclass 表示當前方法的class ,因爲靜態方法屬於類。

上面看到jobject 類型對應java中Object ,jni定義一寫類型和java類型對應起來,如下

//都是java基本類型前面加個j
/* Primitive types that match up with Java equivalents. */
typedef uint8_t  jboolean; /* unsigned 8 bits */  //---->java 中boolean
typedef int8_t   jbyte;    /* signed 8 bits */ //---->java byte
typedef uint16_t jchar;    /* unsigned 16 bits */ 
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */

typedef _jobject*       jobject; // 對應java中的類對象
typedef _jclass*        jclass; // 對應java中的clas
typedef _jstring*       jstring;//對應java中的string
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
…… 省略更多請看jni.h文件中的定義

類如在java中參數爲Int類型的方法

native void test1(int number);

則對應的c++

extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_Test_test1(JNIEnv *env, jobject thiz, jint number) {

}

可以看到在java中int類型參數,在C++中對應的是jint方法

2.獲取java中的屬性和方法(類似java反射)

public class User {
    private String name="Jack";
    private int age=20;

    public String getName() {
        Log.i("User","who在調用我");
        return name;
    }
    
    public void setName(String name) {
        Log.i("User","啊!我被人在調用了");
        this.name = name;
    }
    
    public native void test();
    
}

c++代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_User_test(JNIEnv *env, jobject user) {
    //通過對象獲取類jclass
    jclass userCls = env->GetObjectClass(user);
    //———————————————獲取屬性值———————————————————
    //獲取屬性id 0️⃣
    jfieldID ageId = env->GetFieldID(userCls, "age", "I");
    //通過屬性id獲取屬性值 
    jint ageValue = env->GetIntField(user, ageId);
    //要想在logcat看到值需要引入android/log.h頭文件,使用下面方法打印
    __android_log_print(ANDROID_LOG_INFO,"User","c++,ageValue%d",ageValue);
    //-----------------------
    //同理獲取name屬性值  1️⃣
    jfieldID nameId = env->GetFieldID(userCls, "name", "Ljava/lang/String;");
    //通過屬性id獲取屬性值 String屬性
    jstring nameValue = static_cast<jstring>(env->GetObjectField(user, nameId));
    //———————————————調用方法,無參數———————————————————
    //獲取方法id /2️⃣3️⃣4️⃣5️⃣6️⃣
    jmethodID getNameId = env->GetMethodID(userCls, "getName", "()Ljava/lang/String;");
    //調用getName方法
    jobject callGetName = env->CallObjectMethod(user, getNameId);

    //———————————————調用方法,有參數———————————————————
    //獲取方法id3️⃣
    jmethodID setNameId = env->GetMethodID(userCls, "setName", "(Ljava/lang/String;)V");
    //調用getName方法4️⃣
    env->CallVoidMethod(user, setNameId, env->NewStringUTF("abc"));

    //———————————————創建新對象———————————————————
    //獲取構造方法id5️⃣
    jmethodID initID = env->GetMethodID(userCls, "<init>", "()V");
    //通過c++代碼創建user對象
    jobject newUser = env->NewObject(userCls, initID);
    //c/c++沒有垃圾回收機制,需要自己釋放內存
    env->DeleteLocalRef(newUser);
}

在0️⃣ 使用env->GetFieldID方法獲取屬性id,此方法需要三個參數,第一個是jclass,第二個屬性名,第三個參數寫了個"I"表示int類型簽名,對應關係如下表所示,最新版的Android studio 已經有自動補全功能,當你填寫第二個屬性時,編輯器會自動補全第三個參數類型簽名

java 簽名
int I
char C
byte B
long L
float F
boolean Z
String(類) Ljava/lang/String;
自己寫的類 L類的全路徑;

例如com.wkk.ndkdemo包下面有Data.java 則它的簽名爲Lcom/wkk/ndkdemo/Data;

在2️⃣處使用env->GetMethodID獲取某個方法的id,最後一個參數爲方法簽名,就是把原方法用類型簽名表示,如上面的String getName() 無參數,返回數據類型爲String,所以方法簽名是()Ljava/lang/String;

在3️⃣出 setName 有參數,參數是String類型,無返回值,所以方法簽名爲(Ljava/lang/String;)V 返回值爲viod 用V表示。

在4️⃣ ,通過CallVoidMethod方法調用setName方法,調用java方法都是CallXXXMethod ,xxx表示方法的返回值類型。此類方法前兩個參數分別是jobject,jmethodID,最後一個爲可以變參數。被調用的Java方法參數,setName的參數是String類型,因爲是Java方法,要通過NewStringUTF把字符串轉換爲java可以使用的類型。

我們在c++中可以創建java對象,在5️⃣,調用類的構造方法,每個類的構造方法名都是“"” 並不是像java那樣和類名一樣。通過NewObject創建對象,最後一個參數也是可變參數,添加構造方法的參數值

3. JNI_OnLoad、動態註冊

上面忘記說了要想使用c++需要加載C++庫

在靜態代碼塊中調用

static {
     System.loadLibrary("native-lib")
}

如果是kotlin

   companion object{
        init {
            System.loadLibrary("native-lib")
        }
    }

當調用System.loadLibrary()·方法加載庫時,如果庫中有jint JNI_OnLoad(JavaVM* vm, void* reserved);方法就會先執行這個方法

上面介紹java方法與C++方法關聯的方式一般稱爲靜態註冊。還有一種動態註冊,就是利用JNI_OnLoad 實現。

java 代碼

package com.wkk.jnidemo;

public class Data {

    public native int test();

    public native int test2(int a);
    
}

c++代碼

#include <jni.h>

//如果用不到JNIEnv jobject 兩個參數可以省略不寫
jint test(JNIEnv *env, jobject data) {
    return 10;
}

jint test2(JNIEnv *env, jobject data, jint a) {
    return 1 + a;
}

static const char *CLASS_NAME = "com/wkk/jnidemo/Data";

//JNINativeMethod 是一個結構體
static const JNINativeMethod methods[] = {
        {
                "test",//java中的方法名
                "()I",//對應的方法簽名
                (void *) test//c++中對應的函數指針 轉化爲void 指針
        },
        {
                "test2",
                "(I)I",
                (void *) test2
        }

};


jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    //獲取JNIEnv指針
    jint status = vm->GetEnv((void **) &env, JNI_VERSION_1_6);
    if (status != JNI_OK) {
        //如果獲取失敗 return -1 結束
        return -1;
    }
    //通過env的FindClass方法通過類名獲取jclass
    jclass dataClass = env->FindClass(CLASS_NAME);
    //註冊方法,把java native方法與c++方法關聯
    //第一個參數爲對應的jclass
    //第二個參數是JNINativeMethod 數據組
    //第三個參數表示註冊方法的個數
    jint registerNativesStatus=env->RegisterNatives(dataClass,methods, sizeof(methods)/ sizeof(JNINativeMethod));
    if (registerNativesStatus != JNI_OK) {
        return -1;
    }
    return JNI_VERSION_1_6;
}

以上就是動態註冊的簡單過程。

文章如有解釋不對的地方,往大佬指點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章