JNI開發(二)方法簽名與Java通信

本篇文章將通過以下兩部分內容來介紹JNI開發:

  • Android NDK開發 JNI類型簽名和方法簽名
  • JNI實現java與c/c++相互通訊

一、Android NDK開發 JNI類型簽名和方法簽名

在Java存在兩種數據類型: 基本類型 和 引用類型 ,大家都懂的 。

在JNI的世界裏也存在類似的數據類型,與Java比較起來,其範圍更具嚴格性,如下:

1、primitive types ----基本數據類型,如:int、 float 、char等基本類型

2、reference types----引用類型,如:類、實例、數組。
特別需要注意:數組 ------ 不管是對象數組還是基本類型數組,都作爲reference types存在。
1、primitive types (基本數據類型)映射參見下表:

這些基本數據類型都是可以在Native層直接使用的 。

 

2、reference types (引用數據類型)映射參見下表

注意:

1、引用數據類型則不能直接使用,需要根據JNI函數進行相應的轉換後,才能使用

 

2、多維數組(包括二維數組)都是引用類型,需要使用 jobjectArray 類型存取其值 ;
例如:二維整型數組就是指向一位數組的數組,其聲明使用方式如下:

//獲得一維數組 的類引用,即jintArray類型
    jclass intArrayClass = env->FindClass("[I"); 
    //構造一個指向jintArray類一維數組的對象數組,該對象數組初始大小爲dimion
    jobjectArray obejctIntArray  =  env->NewObjectArray(dimion ,intArrayClass , NULL);
    ...//具體操作

 

類描述符

類描述符是類的完整名稱(包名+類名),將原來的 . 分隔符換成 / 分隔符。

例如:在java代碼中的java.lang.String類的類描述符就是java/lang/String
其實,在實踐中,我發現可以直接用該類型的域描述符取代,也是可以成功的。
例如:

jclass intArrCls = env->FindClass("java/lang/String")

等同於

jclass intArrCls = env->FindClass("Ljava/lang/String;")

數組類型的描述符則爲,則爲: [ + 其類型的域描述符 (後文說明)
例如:

int [ ]     其描述符爲[I

float [ ]   其描述符爲[F

String [ ]  其描述符爲[Ljava/lang/String;

域描述符

1、基本類型的描述符已經被定義好了,如下表所示:

這裏容易搞混淆的是Boolean和long爲什麼不取首字母。因爲byte已經是B,所以B被佔了,Boolean選擇用Z。Long爲什麼不用L,因爲L表示對象,被佔用,選擇用J

 

** 2、引用類型的描述符**

一般引用類型則爲 L + 該類型類描述符 + ; (注意,這兒的分號“;”只得是JNI的一部分,而不是我們漢語中的分段,下同)
例如:String類型的域描述符爲 Ljava/lang/String;
對於數組,其爲 : [ + 其類型的域描述符 + ;

int[ ]     其描述符爲[I
float[ ]   其描述符爲[F
String[ ]  其描述符爲[Ljava/lang/String;
Object[ ]類型的域描述符爲[Ljava/lang/Object;

多維數組則是 n個[ +該類型的域描述符 , N代表的是幾維數組。例如:

int  [ ][ ] 其描述符爲[[I
float[ ][ ] 其描述符爲[[F

方法描述符

將參數類型的域描述符按照申明順序放入一對括號中後跟返回值類型的域描述符,規則如下: (參數的域描述符的疊加)返回類型描述符。對於,沒有返回值的,用V(表示void型)表示。舉例如下:

Java層方法                                               JNI函數簽名

            String test ( )                                         Ljava/lang/String;

            int f (int i, Object object)                            (ILjava/lang/Object;)I

            void set (byte[ ] bytes)                                ([B)V

二、JNI實現java與c/c++相互通訊

2.1、簽名映射表

JNI獲取Java類的方法和字段,都需要一個很重要的參數,就是Java類的方法和字段的簽名。所以最好能夠記住它們。

"Lfully-qualified-class;"->L類全名; 例如Java String類對應的簽名是Ljava/lang/String;

"[type"->java數組的簽名,例如int[]的簽名[I,java Stringg[]的簽名是[Ljava/lang/String;

"(arg-types)ret-type"->(函數參數)返回值,()只是所有參數,ret-type是返回類型簽名例如

void test(String msg)對應的簽名是(Ljava/lang/String;)V

long f(int n,String s,int[] arr)對應的簽名(Ijava/lang/String;[I)J

void f()對應用的簽名()V

2.2、jni是如何訪問java中的方法和字段

jni的native接口中,第一個參數爲JNI接口指針(JNIEnv),第二個參數根據native方法是靜態還是非靜態而不同。非靜態native方法的第二個參數是對該對象的引用,靜態方法的第二參數是對其java類的引用。其與參數對應Java方法參數。

從上述描述中,如果是非靜態的,我們可以拿到對象的引用。通過對象的引用,我們可以訪問該對象的字段和方法。如果是靜態的,我們可以訪問該對象的靜態方法和靜態字段。那麼jni中具體是如何訪問的呢?我們將在下面的章節通過實例來介紹

2.3、jni訪問java中的方法

java代碼

public void show(String s){
    Log.i("MainActivity","show:"+s);
}
public native void showString(String s);

jni代碼

//訪問Java中的show方法
JNIEXPORT void JNICALL Java_com_zzy_ndkdemo_MainActivity_showString(JNIEnv *env, jobject instance, jstring s) {

    //獲取instance的類名稱
    jclass cls = (*env)->GetObjectClass(env,instance);
    if(cls==NULL)
    {
        LOGD("Class %s not found");
    }

    //獲取方法ID,第二個參數爲類名稱,第三個參數爲方法名稱,第三個參數爲方法簽名,詳細參見簽名對照表
    jmethodID id =(*env)->GetMethodID(env,cls,"show","(Ljava/lang/String;)V");
    if(id !=NULL)
    {
        //訪問方法,第二個爲類實例,第三個參數爲方法ID,第四和第四以後爲方法參數,
        // 根據返回類型不同,調用不同的CallXXXMethod方法,xxx返回類型
        (*env)->CallVoidMethod(env,instance,id,s);
    }
}

2.4、jni訪問java中的靜態方法

java代碼

public static void showStatic(String s){
    Log.i("MainActivity","show static:"+s);
}
public native void showStaticString(String s);

jni代碼

//訪問Java中的showStatic靜態方法
JNIEXPORT void JNICALL Java_com_zzy_ndkdemo_MainActivity_showStaticString(JNIEnv *env, jobject instance, jstring s) {

    //獲取instance的類名稱
    jclass cls = (*env)->GetObjectClass(env,instance);
    if(cls==NULL)
    {
        LOGD("Class %s not found");
    }

    //獲取靜態方法ID,第二個參數爲類名稱,第三個參數爲方法名稱,第三個參數爲方法簽名,詳細參見簽名對照表
    jmethodID id =(*env)->GetStaticMethodID(env,cls,"showStatic","(Ljava/lang/String;)V");
    if(id !=NULL)
    {
        //訪問方法,第二個爲類名稱,第三個參數爲方法ID,第四和第四以後爲方法參數,
        // 根據返回類型不同,調用不同的CallStaticXXXMethod方法,xxx返回類型
        (*env)->CallStaticVoidMethod(env,cls,id,s);
    }
}

1、從中我們可以看出靜態方法比非靜態方法的訪問多加了一個static,訪問函數由類實例,變成類引用。

2、如果靜態方法不存在instance類中,我們可以通過FindClass進行訪問,其中第二個參數爲類的全路徑

jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/JniDemo");
if (cls == NULL) {
    LOGD("Class %s not found");
}

2.5、jni訪問java中的字段

java代碼

User user = new User();
user.name = "zhang san";
user.age = 30;
User.token = "2018-2011—3223";

String name = showUserName(user);
Log.i("MainActivity","show name:"+name);
public native String showUserName(User user);

jni代碼

//訪問User中的字段name
JNIEXPORT jstring JNICALL Java_com_zzy_ndkdemo_MainActivity_showUserName(JNIEnv *env, jobject instance, jobject user) {

    //獲取User的類名稱
    jclass cls=(*env)->GetObjectClass(env,user);
    if (cls == NULL) {
        LOGD("Class %s not found");
        return NULL;
    }

    //獲取字段ID,第二個參數類名稱,第三個參數字段名,第三個參數字段簽名,詳細參見簽名對照表
    jfieldID id = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;");
    if(id==NULL)
    {
        LOGD("Field token not found");
        return NULL;
    }
    //獲取字段內容,調用GetXXXField,xxx爲字段類型。除基本類型外,其他的都使用GetObjectField
    jstring name=(jstring) (*env)->GetObjectField(env,user,id);
    return name;
}

2.6、jni訪問Java中的靜態字段

java代碼

User user = new User();
user.name = "zhang san";
user.age = 30;
User.token = "2018-2011—3223";
String token =showUserStaticToken(user);
Log.i("MainActivity","show static token:"+token);
public native String showUserStaticToken(User user);

jni代碼

//訪問User中的靜態字段Token
JNIEXPORT jstring JNICALL Java_com_zzy_ndkdemo_MainActivity_showUserStaticToken(JNIEnv *env, jobject instance, jobject user) {

    //獲取User的類名稱
    jclass cls=(*env)->GetObjectClass(env,user);
    if (cls == NULL) {
        LOGD("Class %s not found");
        return NULL;
    }

    //獲取靜態字段ID,第二個參數類名稱,第三個參數字段名,第三個參數字段簽名,詳細參見簽名對照表
    jfieldID id = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;");
    if(id==NULL)
    {
        LOGD("Field token not found");
        return NULL;
    }

    //獲取字段內容,調用GetStaticXXXField,xxx爲字段類型。除基本類型外,其他的都使用GetStaticObjectField
    jstring token=(*env)->GetStaticObjectField(env,cls,id);
    return token;
}

1、從中我們可以看出靜態字段比非靜態字段的訪問多加了一個static,訪問函數由類實例,變成類引用。

2、如果靜態字段中不存在instance類中,我們可以通過FindClass進行訪問,其中第二個參數爲類的全路徑

jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/User");
if (cls == NULL) {
    LOGD("Class %s not found");
}

2.7、jni中更新java的字段內容

java代碼

User user = updateUser(user);
Log.i("MainActivity","updateUser name:"+user.name+" age:"+user.age+" token:"+User.token);
public native User updateUser(User user);

jni代碼

//更新user內容
JNIEXPORT jobject JNICALL Java_com_zzy_ndkdemo_MainActivity_updateUser(JNIEnv *env, jobject instance,jobject user)
{
    //獲取User的類名稱
    jclass cls=(*env)->GetObjectClass(env,user);
    if (cls == NULL) {
        LOGD("Class %s not found");
        return NULL;
    }

    //獲取每一個字段ID,第二個參數類名稱,第三個參數字段名,第三個參數字段簽名,詳細參見簽名對照表
    jfieldID idName = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;");
    jfieldID idAge = (*env)->GetFieldID(env,cls,"age","I");
    jfieldID idToken = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;");

    //將cha字符串轉成jstring
    jstring name = (*env)->NewStringUTF(env, "李四");
    jstring token = (*env)->NewStringUTF(env, "new token");

    //更新字段內容,調用setxxxField,xxx爲字段類型,如果是靜態字段還需要加上static
    //第二個參數爲需要修改的類實例或則類名,第三個參數爲字段Id,第四個參數爲需要修改的內容
    (*env)->SetObjectField(env,user,idName,name);
    (*env)->SetIntField(env,user,idAge,20);
    (*env)->SetStaticObjectField(env,cls,idToken,token);

    return user;
}

2.8、jni中創建java類實例

java代碼

User user = createUser();
Log.i("MainActivity","createUser name:"+user.name+" age:"+user.age+" token:"+User.token);
public native User createUser();

jni代碼

//創建user實例
JNIEXPORT jobject JNICALL Java_com_zzy_ndkdemo_MainActivity_createUser(JNIEnv *env, jobject instance)
{
    //在指定路徑查找到USer類名稱
    jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/User");
    if (cls == NULL) {
        LOGD("Class %s not found");
    }

    //獲取User類的構造方法ID,第二個參數爲類名稱,第三個參數固定爲"<init>"
    //第四個參數爲構造函數簽名,詳細參見簽名對照表
    jmethodID id= (*env)->GetMethodID(env, cls, "<init>", "()V");

    //實例化User類,第二個參數類名稱,第三個參數構造方法ID
    jobject user = (*env)->NewObject(env, cls, id);
    if (user == NULL) {
        LOGD("Create User failed");
    }

    //獲取每一個字段ID,第二個參數類名稱,第三個參數字段名,第三個參數字段簽名,詳細參見簽名對照表
    jfieldID idName = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;");
    jfieldID idAge = (*env)->GetFieldID(env,cls,"age","I");
    jfieldID idToken = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;");

    //將cha字符串轉成jstring
    jstring name = (*env)->NewStringUTF(env, "王五");
    jstring token = (*env)->NewStringUTF(env, "second token");

    //賦予字段內容,調用setxxxField,xxx爲字段類型,如果是靜態字段還需要加上static
    //第二個參數爲需要修改的類實例或則類名,第三個參數爲字段Id,第四個參數爲需要修改的內容
    (*env)->SetObjectField(env,user,idName,name);
    (*env)->SetIntField(env,user,idAge,10);
    (*env)->SetStaticObjectField(env,cls,idToken,token);

    return user;
}

2.9、jni中的異常

JNI中也有異常,不過它和C++、Java的異常不太一樣。當調用JNIEnv的某些函數出錯後,會產生一個異常,但這個異常不會中斷本地函數的執行,直到從JNI層返回到Java層後,虛擬機纔會拋出這個異常。雖然在JNI層中產生的異常不會中斷本地函數的運行,但一旦產生異常後,就只能做一些資源清理工作了(例如釋放全局引用,或者ReleaseStringChars)。如果這時調用除上面所說函數之外的其他JNIEnv函數,則會導致程序死掉。JNIEnv提供了三個函數進行幫助:

1、ExceptionOccured函數,用來判斷是否發生異常。

2、ExceptionClear函數,用來清理當前JNI層中發生的異常。

3、ThrowNew函數,用來向Java層拋出異常。

int jniCheckException(JNIEnv *env) {
    jthrowable ex = (*env)->ExceptionOccurred(env);
    if (ex) {
        (*env)->ExceptionDescribe(env);
        (*env)->ExceptionClear(env);
        (*env)->DeleteLocalRef(env, ex);
        return 1;
    }
    return 0;
}

2.10、常用函數封裝

在JNI中訪問方法和字段,都需要走好幾個步驟,如果其中一個步驟失敗了,就會導致整個訪問過程的失敗、甚至死機。所以我們在每個函數的調用過程中,儘量對其做異常檢測。以下爲常用函數的封裝。

jobject jniGlobalRef(JNIEnv *env, jobject cls) {
    jobject gcls = (*env)->NewGlobalRef(env, cls);
    if (gcls == NULL)
        LOGE("Global ref failed (out of memory?)");
    return gcls;
}

jclass jniFindClass(JNIEnv *env, const char *name) {
    jclass cls = (*env)->FindClass(env, name);
    if (cls == NULL)
        LOGE("Class %s not found", name);
    else
        jniCheckException(env);
    return cls;
}

jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature) {
    jmethodID method = (*env)->GetMethodID(env, cls, name, signature);
    if (method == NULL) {
        LOGE("Method %s %s not found", name, signature);
        jniCheckException(env);
    }
    return method;
}

jfieldID jniGetFieldID(JNIEnv *env, jclass cls, const char *name, const char *type) {
    jfieldID field = (*env)->GetFieldID(env, cls, name, type);
    if (field == NULL)
        LOGE("Field %s type %s not found", name, type);
    return field;
}

jobject jniNewObject(JNIEnv *env, jclass cls, jmethodID constructor, const char *name) {
    jobject object = (*env)->NewObject(env, cls, constructor);
    if (object == NULL)
        LOGE("Create object %s failed", name);
    else
        jniCheckException(env);
    return object;
}

2.11、總結

JNI 程序開發者要遵循 native 語言本身的內存管理機制,避免造成內存泄漏。以 C 語言爲例,當用 malloc() 在進程堆中動態分配內存時,JNI 程序在使用完後,應當調用 free() 將內存釋放。總之,所有在 native 語言編程中應當注意的內存泄漏規則,在 JNI 編程中依然適應。Native 語言本身引入的內存泄漏會造成 native memory 的內存,嚴重情況下會造成 native memory 的 out of memory。如上述中使用到的NewStringUTF和NewObject在使用完成後,都需要進行釋放。
Demo:https://github.com/zhao007z4/NDKDemo

參考 https://blog.csdn.net/zhao007z5/article/details/80066366
https://www.cnblogs.com/mingfeng002/p/6595047.html

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