Android NDK開發之旅11--JNI--JNI數據類型與方法屬性訪問

JNI數據類型

JNI的數據類型包含兩種: 基本類型和引用類型

基本類型

基本類型主要有jboolean, jchar, jint等, 它們和Java中的數據類型對應關系如下表所示:

Java類型 JNI類型 描述
boolean jboolean 無符號8位整型
byte byte 無符號8位整型
char jchar 無符號16位整型
short jshort 有符號16位整型
int jint 32位整型
long jlong 64位整型
float jfloat 32位浮點型
double jdouble 64位浮點型
void void 無類型

引用類型(對象)

JNI中的引用類型主要有類, 對象和數組. 它們和Java中的引用類型的對應關系如下表所示:

Java類型 JNI類型 描述
Object jobject Object類型
Class jclass Class類型
String jstring String類型
Object[] jobjectArray 對象數組
boolean[] jbooleanArray boolean數組
byte[] jbyteArray byte數組
char[] jcharArray char數組
short[] jshortArray short數組
int[] jintArray int數組
long[] jlongArray long數組
float[] jfloatArray float數組
double[] jdoubleArray double數組
Throwable jthrowable Throwable

native函數參數說明

每個native函數,都至少有兩個參數(JNIEnv*,jclass或者jobject)。

1)當native方法為靜態方法時:
jclass 代表native方法所屬類的class對象(JniTest.class)。

2)當native方法為非靜態方法時:
jobject 代表native方法所屬的對象。

native函數的頭文件可以自己寫。

關于屬性與方法的簽名

數據類型 簽名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
ully-qualified-class Lfully-qualified-class;
type[] [type
method type (arg-types)ret-type
注意:
  • 類描述符開頭的 'L' 與結尾的 ';' 必須要有
  • 數組描述符,開頭的 '[' 必須要有
  • 方法描述符規則: "(各參數描述符)返回值描述符",其中參數描述符間沒有任何分隔符號

從上表可以看出, 基本數據類型的簽名基本都是單詞的首字母大寫, 但是boolean和long除外因為B已經被byte占用, 而long也被Java類簽名的占用.

對象和數組的簽名稍微復雜一些.

對象的簽名就是對象所屬的類簽名, 比如String對象, 它的簽名為Ljava/lang/String; .

數組的簽名為[+類型簽名, 例如byte數組. 其類型為byte, 而byte的簽名為B, 所以byte數組的簽名就是[B.同理可以得到如下的簽名對應關系:

char[]      [C
float[]     [F
double[]    [D
long[]      [J
String[]    [Ljava/lang/String;
Object[]    [Ljava/lang/Object;

方法簽名具體方法:
獲取方法的簽名比較麻煩一些,通過下面的方法也可以拿到屬性的簽名。
打開命令行,輸入javap,出現以下信息:

javap命令

上述信息告訴我們,通過以下命令就可以拿到指定類的所有屬性、方法的簽名了,很方便有木有?!

javap -s -p 完整類名

我們通過cd命令,來到編譯生成的class字節碼文件目錄(注意:非src目錄。 eclipse 編譯生成的class字節碼文件在bin文件夾中, 而用idea編譯器 編譯生成的class字節碼文件在out\production下),然后輸入命令:

D:\IdeaProjects\jni1\out\production\jni1>javap -s -p com.haocai.jni.JniTest 

得到以下信息:

Compiled from "JniTest.java"
public class com.haocai.jni.JniTest {
  public java.lang.String key;
    descriptor: Ljava/lang/String;
  public static int count;
    descriptor: I
  public com.haocai.jni.JniTest();
    descriptor: ()V

  public static native java.lang.String getStringFromC();
    descriptor: ()Ljava/lang/String;

  public native java.lang.String getString2FromC(int);
    descriptor: (I)Ljava/lang/String;

  public native java.lang.String accessField();
    descriptor: ()Ljava/lang/String;

  public native void accessStaticField();
    descriptor: ()V

  public native void accessMethod();
    descriptor: ()V

  public native void accessStaticMethod();
    descriptor: ()V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  public int genRandomInt(int);
    descriptor: (I)I

  public static java.lang.String getUUID();
    descriptor: ()Ljava/lang/String;

  static {};
    descriptor: ()V
}
其中,descriptor:對應的值就是我們需要的簽名了,注意簽名中末尾的分號 ";" 不能省略。

C/C++訪問Java的屬性、方法

在JNI調用中,肯定會涉及到本地方法操作Java類中數據和方法。在Java1.0中“原始的”Java到C的綁定中,程序員可以直接訪問對象數據域。然而,直接方法要求虛擬機暴露他們的內部數據布局,基于這個原因,JNI要求程序員通過特殊的JNI函數來獲取和設置數據以及調用java方法。

有以下幾種情況:

1.訪問Java類的非靜態屬性。
2.訪問Java類的靜態屬性。
3.訪問Java類的非靜態方法。
4.訪問Java類的靜態方法。
5.間接訪問Java類的父類的方法。
6.訪問Java類的構造方法。

一、訪問Java的非靜態屬性

Java聲明如下:

 public String name = "kpioneer";

//訪問非靜態屬性name,修改它的值 
//accessField 自定義的一個方法
public native void accessField();

C代碼如下:

//把java中的變量name中值kpioneer 變為kpioneer Goodman
JNIEXPORT jstring JNICALL Java_com_haocai_jni_JniTest_accessField
(JNIEnv *env, jobject jobject) {
    //Jclass 
    //jobj是t對象
    jclass cls = (*env)->GetObjectClass(env, jobject);
    //jfieldID
    //屬性名稱,屬性簽名
    jfieldID fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");


    //類似于反射
    //拿到jniTest(jobject) 中name的值
    /*
    Get<Type>Field:
    GetFloatField
    GetIntField
    GetLongField
    ...
    */
    jstring jstr = (*env)->GetObjectField(env, jobject, fid);
    printf("jstr:%#x\n", &jstr);
    //printf("jstr:%#x\n", &jstr);
    //jstring -> C 字符串

    boolean isCopy =NULL;
    //函數內部復制了,isCopy 為JNI_TRUE,沒有復制JNI_FALSE
    char *c_str = (*env)->GetStringUTFChars(env, jstr, &isCopy );
    //意義:isCopy為JNI_FALSE,c_str和jstr都指向同一個字符串,不能修改java字符串

    char text[20] = " Goodman";
    strcat(c_str, text);//拼接函數

    //再把C字符串 ->jstring
    jstring new_str = (*env)->NewStringUTF(env, c_str);
    printf("jstr:%#x\n", &new_str);
    //修改name
    /*
    Set<Type>Field:
    SetFloatField
    SetIntField
    SetLongField
    ...
    */
    (*env)->SetObjectField(env, jobject, fid, new_str);

    //最后釋放資源,通知垃圾回收器來回收
    //良好的習慣就是,每次GetStringUTFChars,結束的時候都有一個ReleaseStringUTFChars與之呼應
    (*env)->ReleaseStringUTFChars(env, jstr, c_str);
    return new_str;
}

最后在Java中測試:

public static void main(String[] args) {
        JniTest jniTest = new JniTest();
        System.out.println("name修改前:"+jniTest.name);
        jniTest.accessField();
        System.out.println("name修改后:"+jniTest.name);
}

結果輸出:
name修改前:kpioneer
name修改后:kpioneer Goodman
jstr:0x27cf238  //調用的C也打印輸出
jstr:0x27cf2a8  

二、訪問Java的靜態屬性

Java聲明如下:

public static int count = 9;
public native void accessStaticField();

C代碼如下:

//訪問靜態屬性
JNIEXPORT void JNICALL Java_com_haocai_jni_JniTest_accessStaticField
(JNIEnv *env, jobject jobj) {
    //jclass
    jclass cls = (*env)->GetObjectClass(env, jobj);
    //jfieldID
    jfieldID fid =(*env)->GetStaticFieldID(env, cls, "count", "I");
    //GetStatic<Type>Field
    jint count = (*env)->GetStaticIntField(env, cls, fid);
    count++;
    //修改
    //SetStatic<Type>Field
    (*env)->SetStaticIntField(env, cls, fid, count);
}

最后在Java中測試:

public static void main(String[] args) {
    JniTest jniTest= new JniTest();
    System.out.println("count修改前:"+count);
    jniTest.accessStaticField();
    System.out.println("count修改后:"+count);
}

結果輸出:
count修改前:9
count修改后:10

三、訪問Java的非靜態方法

Java聲明如下:

    //產生指定范圍的隨機數
    public int genRandomInt(int max){
        System.out.println("genRandomInt 執行了..");
        return new Random().nextInt(max);
    }

C代碼如下:

JNIEXPORT void JNICALL Java_com_haocai_jni_JniTest_accessMethod
(JNIEnv *env, jobject jobj) {

    //Jclass
    jclass cls = (*env)->GetObjectClass(env, jobj);
    //JmethodID
    jfieldID mFid = (*env)->GetMethodID(env, cls, "genRandomInt", "(I)I");
    //調用
    //Call<Type>Method
    jint random = (*env)->CallIntMethod(env, jobj, mFid, 200);
    printf("random num:%ld",random);

}

最后在Java中測試:

public static void main(String[] args) {
    JniTest jniTest= new JniTest();
    jniTest.accessMethod();
}
結果輸出:
genRandomInt 執行了..

random num:109

四、訪問Java的靜態方法

Java聲明如下:

    public static  String getUUID(){
      return  UUID.randomUUID().toString();
    }

C代碼如下:

//訪問Java靜態方法
JNIEXPORT void JNICALL Java_com_haocai_jni_JniTest_accessStaticMethod
(JNIEnv *env, jobject jobj) {

    //Jclass
    jclass cls = (*env)->GetObjectClass(env, jobj);
    //JmethodID
    jfieldID mFid = (*env)->GetStaticMethodID(env, cls, "getUUID", "()Ljava/lang/String;");

    //調用
    //CallStatic<Type>Method
    jstring uuid = (*env)->CallStaticObjectMethod(env, jobj, mFid);
    
    //隨機文件名稱 uuid.txt
    //jstring -> char*
    //isCopy JNI_FALSE,代表java和c操作的是同一個字符串
    char *uuid_str = (*env)->GetStringUTFChars(env, uuid, NULL);
    //拼接
    char filename[100];
    sprintf(filename, "D://%s.txt", uuid_str);
    FILE *fp = fopen(filename, "w");
    fputs("i love kpioneer", fp);
    fclose(fp);


}

最后在Java中測試:

public static void main(String[] args) {
        JniTest jniTest = new JniTest();

        jniTest.accessStaticMethod();.
}
最終在D盤目錄下生成名為2fbf3e41-741b-4899-8e4e-a6a80a23a0b2(UUID隨機生成) 的txt文件

五、訪問Java類的構造方法

Java聲明如下:

    public native long accessConstructor();

C代碼如下:

//訪問Java類的構造方法
//使用java.util.Date產生一個當前的事件戳
JNIEXPORT jlong  JNICALL Java_com_haocai_jni_JniTest_accesssConstructor
(JNIEnv *env, jobject jobj) {

    jclass cls = (*env)->FindClass(env, "java/util/Date");
    //jmethodID
    jmethodID  constructor_mid= (*env)->GetMethodID(env, cls,"<init>","()V");
    //實例化一個Date對象(可以在constructor_mid后加參)
    jobject date_obj =  (*env)->NewObject(env, cls, constructor_mid);
    //調用getTime方法
    jmethodID mid = (*env)->GetMethodID(env, cls, "getTime", "()J");
    jlong time = (*env)->CallLongMethod(env, date_obj, mid);

    printf("time:%lld\n",time);

    return time;

}

最后在Java中測試:

public static void main(String[] args) {

        JniTest test = new JniTest();
        //直接在Java中構造Date然后調用getTime
        Date date = new Date();
        System.out.println(date.getTime());
        //通過C語音構造Date然后調用getTime
        long time = jniTest.accessConstructor();
        System.out.println(time);
}


結果輸出:
1509688828013
1509688828013

time:1509688828013

六、間接訪問Java類的父類的方法

Java代碼如下:
父類:

public class Human {
    public void sayHi(){
        System.out.println("人類打招乎(父類)");
    }
}

子類:

public class Man extends Human {
    @Override
    public void sayHi() {
        System.out.println("男人打招乎");
    }
}

在TestJni類中有Human方法聲明:

    public Human human = new Man();

    public native void accessNonvirtualMethod();

如果是直接使用human .sayHi()的話,其實訪問的是子類Man的方法
但是通過底層C的方式可以間接訪問到父類Human的方法,跳過子類的實現,甚至你可以直接哪個父類(如果父類有多個的話),這是Java做不到的。

下面是C代碼實現,無非就是屬性和方法的訪問:

//調用父類的方法
JNIEXPORT void JNICALL Java_com_haocai_jni_JniTest_accessNonvirtualMethod
(JNIEnv *env, jobject jobj) {

    jclass cls = (*env)->GetObjectClass(env, jobj);

    //獲取man屬性(對象)
    jfieldID fid = (*env)->GetFieldID(env, cls, "human", "Lcom/haocai/jni/Human;");
    //獲取
    jobject human_obj = (*env)->GetObjectField(env, jobj, fid);

    //執行sayHi方法
    jclass human_cls = (*env)->FindClass(env, "com/haocai/jni/Human");
    jmethodID mid = (*env)->GetMethodID(env, human_cls, "sayHi", "()V");
    
    //執行Java相關的子類方法
    (*env)->CallObjectMethod(env, human_obj, mid);

    //執行Java相關的父類方法
    (*env)->CallNonvirtualObjectMethod(env, human_obj, human_cls, mid);

}

1.當有這個類的對象的時候,使用(env)->GetObjectClass(),相當于Java中的test.getClass()
2.當有沒有這個類的對象的時候,(
env)->FindClass(),相當于Java中的Class.forName("com.test.TestJni")
這里直接使用CallVoidMethod,雖然傳進去的是父類的Method ID,但是訪問的讓然是子類的實現。

最后,通過CallNonvirtualVoidMethod,訪問不覆蓋的父類方法(C++使用virtual關鍵字來覆蓋父類的實現),當然你也可以指定哪個父類(如果有多個父類的話)。

最后在Java中測試:

    public static void main(String[] args) {
        JniTest jniTest = new JniTest();
        jniTest.human.sayHi();
        jniTest.accessNonvirtualMethod();

    }

結果輸出:
男人打招乎
男人打招乎  
人類打招乎(父類) 

實際案例---用JNI方法和屬性訪問解決中文編碼亂碼問題

中文亂碼

    char *cOutStr = "李四";
    string jstr = (*env)->NewStringUTF(env, cOutStr);
    return jstr; 直接返回會有中文亂碼問題
原因分析,調用NewStringUTF的時候,產生的是UTF-16的字符串,但是我們需要的時候UTF-8字符串。
如果使用C語言方法解決中文編碼問題,代碼行數多(幾百行+),且容易產生問題。所以直接通過Java 中的String(byte bytes[],String charsetName)構造方法來進行字符集變換,解決該問題。

Java聲明如下:

  public native String chineseChars(String str);

C代碼如下:

JNIEXPORT jstring JNICALL Java_com_haocai_jni_JniTest_chineseChars
(JNIEnv *env, jobject jobj,jstring in) {

//輸出
    char *cStr = (*env)->GetStringUTFChars(env, in, JNI_FALSE);
    printf("C %s\n", cStr);


    //c -> jstring
    char *cOutStr = "李四";
    //jstring jstr = (*env)->NewStringUTF(env, cOutStr);
    //return jstr; 直接返回會有中文亂碼問題

    //解決中文亂碼問題
    //執行java 中String(byte bytes[],String charsetName);
    //1.jmethodID
    //2.byte數組
    //3.字符編碼

    jstring str_cls = (*env)->FindClass(env, "java/lang/String");
    //構造方法用<init>
    jmethodID construvtor_mid =  (*env)->GetMethodID(env, str_cls, "<init>", "([BLjava/lang/String;)V");

    //jbyte-> char
    //jbyteArray -> char[]
    jbyteArray bytes = (*env)->NewByteArray(env, strlen(cOutStr));
    //byte數組賦值
    //從0到strlen(cOutStr),從頭到尾
    (*env)->SetByteArrayRegion(env,bytes,0,strlen(cOutStr), cOutStr);

    //字符編碼jstring
    jstring charsetName = (*env)->NewStringUTF(env, "GB2312");

    //調用構造函數,返回編碼之后的jstring


    return (*env)->NewObject(env,str_cls, construvtor_mid,bytes, charsetName);

}

最后在Java中測試:

    public static void main(String[] args) {
       JniTest jniTest = new JniTest();
       String outStr = jniTest.chineseChars("張三");
       System.out.println("中文輸出:"+outStr);
    }

結果輸出:
中文輸出:李四

C 張三

總結

  • 1.C/C++完成的功能并不是所有代碼一定要C/C++語句寫,有時候C/C++可以調用現成的Java方法或屬性解決問題,能起到事半功倍的作用。
  • 2.屬性、方法的訪問的使用是和Java的反射相類似。

特別感謝:
動腦學院Jason

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容