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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容