Android-JNI開發系列《六》jni與java的互動

2020-10-26 12:01:33

人間觀察
1024-程式設計師節
願各位程式設計師歷盡千帆,歸來仍是少年。

這片文章本來不打算寫的,因為在前面的文章多多少少的提到了jni和java的互動,但是為了讓知識體系更健全寫,還是梳理下,算是jni和java的在互動上的一個總結吧。
兩者的互動歸納起來主要就是兩種。

  1. java呼叫jni。比如:傳遞基本資料,複雜物件等
  2. jni呼叫java。比如回撥,異常,呼叫java方法/成員變數,構造java物件等等

java呼叫jni-傳到複雜物件到jni中

我們新建一個java的物件,然後傳遞到jni中,在jni中獲取該物件的屬性值。

java物件如下

package com.bj.gxz.jniapp.methodfield;

import java.io.Serializable;

/**
 * Created by guxiuzhong on 2020/10/24.
 */
public class AppInfo implements Serializable {
    private static final String TAG = "AppInfo";
    private String versionName;
    public int versionCode;
    public long size;

    public AppInfo(String versionName) {
        this.versionName = versionName;
    }

    public AppInfo(String versionName, int versionCode) {
        this.versionName = versionName;
        this.versionCode = versionCode;
    }

    public String getVersionName() {
        return versionName;
    }

    public void setVersionName(String versionName) {
        this.versionName = versionName;
    }

    public int getVersionCode() {
        return versionCode;
    }

    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }

    public void setSize(long size) {
        this.size = size;
    }

    public long getSize() {
        return size;
    }

    @Override
    public String toString() {
        return "AppInfo{" +
                "versionName='" + versionName + '\'' +
                ", versionCode=" + versionCode +
                ", size=" + size +
                '}';
    }
}

jni介面為

public native void getAppInfoFromJava(AppInfo appInfo);

對應jni的方法是

extern "C" JNIEXPORT void  JNICALL
Java_com_bj_gxz_jniapp_methodfield_JNIMethodField_getAppInfoFromJava(JNIEnv *env, jobject instance,
                                                                     jobject obj) {
                 // ...                                                    
}

最後一個引數obj就是對應java傳過來的物件AppInfo。 因為在jni中所有的java物件都是jobject。對應關係,這裡再貼一下:

基本資料型別

java與Native對映關係如下表所示:

Java型別Native 型別Description
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjint signed32 bits
long jlongsigned64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidnot applicable

參照資料型別

外面的為jni中的,括號中的java中的。

  • jobject
    • jclass (java.lang.Class objects)
    • jstring (java.lang.String objects)
    • jarray (arrays)
      • jobjectArray (object arrays)
      • jbooleanArray (boolean arrays)
      • jbyteArray (byte arrays)
      • jcharArray (char arrays)
      • jshortArray (short arrays)
      • jintArray (int arrays)
      • jlongArray (long arrays)
      • jfloatArray (float arrays)
      • jdoubleArray (double arrays)
  • jthrowable (java.lang.Throwable objects)

上面的層次中的jni的參照型別代表了繼承關係,jbooleanArray繼承jarray,jarray繼承jobject,最終都繼承jobject。

ok。

jni呼叫java物件的方法

呼叫物件的某個方法 Call<返回型別>Method<傳參型別>,比如呼叫AppInfogetVersionCode對應的就是CallIntMethod,呼叫setVersionCode對應的就是CallVoidMethod方法,

Call<返回型別>Method<傳參型別>Native 型別java型別
CallVoidMethod() CallVoidMethodA() CallVoidMethodV()voidvoid
CallObjectMethod() CallObjectMethodA() CallObjectMethodV()jobjectobject
CallBooleanMethod() CallBooleanMethodA() CallBooleanMethodV()jbooleanboolean
CallByteMethod() CallByteMethodA() CallByteMethodV()jbytebyte
CallCharMethod() CallCharMethodA() CallCharMethodV()jcharchar
CallShortMethod() CallShortMethodA() CallShortMethodV()jshortshort
CallIntMethod() CallIntMethodA() CallIntMethodV()jintint
CallLongMethod() CallLongMethodA() CallLongMethodV()jlonglong
CallFloatMethod() CallFloatMethod A() CallFloatMethodV()jlonglong
CallDoubleMethod() CallDoubleMethodA() CallDoubleMethodV()jfloatfloat
如果java方法是靜態的如下--
CallStaticShortMethod() CallStaticShortMethodA() CallStaticShortMethodV()jshortshort
省略其它方法…--

具體可以參考官網:官網開發檔案

每一種返回型別對應3個方法,唯一不同的是輸入引數的傳入形式不同。所以getVersionName對應的就是CallObjectMethod

CallObjectMethod引數:
obj:某個 Java 物件範例
methodID:指定方法的ID
args:輸入參數列,方法如果沒有引數則不寫

特別注意
如果你呼叫的是Java物件的方法CallxxxxMethod第一個引數是某個 Java 物件範例。但是如果你呼叫的是靜態方法,則第一個引數是jclass。靜態方法並不屬於某一個物件。

methodID的獲取通過GetMethodID,在jni.h標頭檔案中函數宣告原型為:

jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)

GetMethodID引數
clazz: 對應java的class類; 為java類的全類名比如把點改為/即com.bj.gxz.jniapp.methodfield.AppInfo改為 com/bj/gxz/jniapp/methodfield/AppInfo
const char* name 方法名字;
const char* sig 方法的簽名,方法的簽名可以通過javap -s xxx.class獲取。

範例獲取getVersionName的完整程式碼如下:

    // 根據java物件獲取物件對應的class
    jclass cls = env->GetObjectClass(obj);
    // 獲取&呼叫java方法
    jmethodID getVersionName_mid = env->GetMethodID(cls, "getVersionName", "()Ljava/lang/String;");
    jstring versionName = (jstring) env->CallObjectMethod(obj, getVersionName_mid);
        char *c_string = const_cast<char *>(env->GetStringUTFChars(versionName, 0));
    LOG_D("versionName=%s", c_string);

看著很簡單吧。

jni獲取java物件的屬性值

獲取java物件的屬性對應的值的方法為GetXXXXField,XXXX 為資料型別,比如GetIntFieldGetShortField等等,如果不是基本型則為GetObjectField,如果屬性為static的則加上Static,比如GetStaticIntFieldGetStaticObjectField。在jni中是不看成員變數的作用域的,不管你是privateprotectedpublic的,加finnal也一樣,它都可以讀取裡面的值,和反射不一樣。

GetXXXXField引數
obj:某個 Java 物件範例
jfieldID:指定屬性的ID

GetFieldID引數
clazz:物件java物件的class
const char* name:屬性名字
const char* sig:資料型別描述符

資料型別描述符java和jni的對應關係如下,來:

Java型別型別描述符
intI
longJ
byteB
shortS
charC
floatF
doubleD
booleanZ
voidV
陣列[
二維陣列[[
其他參照型別L+類全名+;

例子如下:

    // 獲取java物件的屬性值
    jfieldID versionCode_fieldId = env->GetFieldID(cls, "versionCode", "I");
    int versionCode = env->GetIntField(obj, versionCode_fieldId);
    LOG_D("versionCode=%d", versionCode);

    // 獲取java物件的屬性值
    jfieldID size_fieldId = env->GetFieldID(cls, "size", "J");
    long size = env->GetLongField(obj, size_fieldId);
    LOG_D("size=%ld", size);

    // 獲取java靜態屬性的值
    jfieldID tag_fieldId = env->GetStaticFieldID(cls, "TAG", "Ljava/lang/String;");
    jstring tag_java = (jstring) env->GetStaticObjectField(cls, tag_fieldId);
    char *tag_c_string = const_cast<char *>(env->GetStringUTFChars(tag_java, 0));
    LOG_D("TAG=%s", tag_c_string);

執行後:

        JNIMethodField jniMethodField = new JNIMethodField();

        AppInfo javaInfo = new AppInfo("com.wg.com", 30);
        javaInfo.setSize(500);
        jniMethodField.getAppInfoFromJava(javaInfo);
10-23 23:25:27.572 2900-2900/com.bj.gxz.jniapp D/JNI: versionName=com.wg.com
10-23 23:25:27.572 2900-2900/com.bj.gxz.jniapp D/JNI: versionCode=30
10-23 23:25:27.572 2900-2900/com.bj.gxz.jniapp D/JNI: size=500
10-23 23:25:27.572 2900-2900/com.bj.gxz.jniapp D/JNI: TAG=AppInfo

屬性的獲取和方法的獲取都差不多。

jni中構造java物件&呼叫java方法

關於的回撥異常可以參考前面的文章。

Android-JNI開發系列《二》-在jni層的執行緒中回撥到java層

Android-JNI開發系列《三》-例外處理

我們在jni中建立一個java物件,其實就是呼叫java的構造方法,只不過是構造方法的函數簽名為固定值<init>. 在jni中建立複雜物件(任何java物件)用NewObject方法,同樣有不同引數的NewObjectVNewObjectA

jni.h函數宣告原型為:

 jobject NewObject(jclass clazz, jmethodID methodID, ...)

引數
clazz java類的class;
methodID 指定方法的ID;
… 引數 支援多個引數;

clazz和methodID同上文方法中的介紹。

範例如下:

    // 獲取java的class
    jclass cls = env->FindClass("com/bj/gxz/jniapp/methodfield/AppInfo");

    // 建立java物件,就是呼叫構造方法,構造方法的方法簽名固定為<init>
    jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;)V");
    jobject obj = env->NewObject(cls, mid, env->NewStringUTF("com.gxz.com"));

    // 給定方法名字和簽名,呼叫方法
    jmethodID setVersionCode_mid = env->GetMethodID(cls, "setVersionCode", "(I)V");
    env->CallVoidMethod(obj, setVersionCode_mid, 1);

    // 給定屬性名字和簽名,設定屬性的值
    jfieldID size_field_id = env->GetFieldID(cls, "size", "J");
    env->SetLongField(obj, size_field_id, (jlong) 1000);

size的屬性我們是通過SetLongField設定的,見名知義。這個和獲取屬性一樣/呼叫java中的方法一樣,它也有類似SetLongFieldSetIntField,SetStaticIntField,SetObjectField等等,區分了基本型別,靜態屬性,參照型別。大家可以嘗試一下,這裡不多介紹了。
執行後:

        AppInfo info = jniMethodField.createAppInfoFromJni();
        Log.e(TAG, "info=" + info);
        

輸出

        10-23 23:25:27.572 2900-2900/com.bj.gxz.jniapp E/JNI: info=AppInfo{versionName='com.gxz.com', versionCode=1, size=1000}

最後原始碼:https://github.com/ta893115871/JNIAPP