JNI开发系列③C语言调用Java字段与方法

接续上篇JNI开发系列②.h头文件分析

前情提要

在前面 , 我们已经熟悉了JNI的开发流程 , .h头文件的分析 , 生成头文件javah命令 , 以及java类型在C语言中的表现形式 , 值得注意的是 , java中的所有引用类型都是jobject类型 , native生成的函数 , 以Java_全类名_方法名表示,包名的._表示 。

概述

在开篇的时候 ,我们就使用java的native方法调用过C函数 , 返回了一个String类型的字符串 , 使用(*Env)->NewStringUTF(Env, "Jni C String");函数 , 我们将字符指针转换成jstring , java类型的字符串返回给了我们的java层 。今天我们来学习 , 使用C语言来调用Java的字段与方法 。

part 1 : C 函数访问java字段

一 , 定义Java 的String类型字段与修改字段的native方法

// 使用C语言修改java字段
public String name = "zeno" ;

// C语言修改java String 类型字段本地方法
public native void accessJavaStringField() ;

// 调用
HelloJni jni = new HelloJni() ;
System.out.println("修改前 name 的值:"+jni.name);
//C语言修改java字段本地方法
jni.accessJavaStringField();
System.out.println("修改后 name 的值:"+jni.name);

二 , 在C语言头文件中定义native方法的实现函数 , 并实现

// com.zeno.jni_HelloJNI.h
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *, jobject);

// Hello_JNI.c

/*C语言访问java String类型字段*/
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *env, jobject jobj) {

    // 得到jclass
    jclass jcls = (*env)->GetObjectClass(env, jobj); 

    // 得到字段ID
    jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");

    // 得到字段的值
    jstring jstr = (*env)->GetObjectField(env, jobj, jfID);

    // 将jstring类型转换成字符指针
    char* cstr = (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
    //printf("is vaule:%s\n", cstr);
    // 拼接字符
    char text[30] = "  xiaojiu and ";
    strcat(text, cstr);
    //printf("modify value %s\n", text);

    // 将字符指针转换成jstring类型
    jstring new_str = (*env)->NewStringUTF(env, text);

    // 将jstring类型的变量 , 设置到java 字段中
    (*env)->SetObjectField(env, jobj, jfID, new_str);
}

三 , 输出

修改前 name 的值:zeno
修改后 name 的值:  xiaojiu and zeno

四 , 分析

首先来分析C函数:

JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *env, jobject jobj)

JNIEXPORT jstring JNICALL Java_com_zeno_jni_HelloJni_getStringFromC
(JNIEnv *Env, jclass jclazz)

我们可以看出两处不同 , 一处是返回值类型 , 一处是函数参数类型 ,返回值类型没什么可讲的 , 在分析.h头文件的时候 , 已经详细讲述了 。那 , 这两个函数参数类型jobjectjclass有什么区别呢 ? 这两个类型表示 , Java的native函数 , 是成员方法还是类方法 , 成员方法需要对象.方法名 , 类方法则类名.方法名 , 可以在main方法里面直接使用 。

接下来是:

// 得到jclass jclass就好比java的.class对象
jclass jcls = (*env)->GetObjectClass(env, jobj);

为什么要得到jclass呢 ?
因为 ,我们要获取字段ID , 在JNI中 , 获取java字段与方法都需要签名。而签名是在类加载的时候完成 , 所以在获取字段ID的时候需要传入jclass 。

// 得到字段ID

jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");

通过传入jclass , 字段名称 , 字段签名 , 就可以得到字段ID ,也可使用GetMethodID函数得到方法ID 。

为什么传入了字段名称,还需要签名呢 ?
因为java支持重载 , 一个方法名称可以有多个不同实现 , 根据传入的参数不同 ,所以C语言调用函数为了区分不同的方法, 而对每个方法做了签名 , 而字段则可用来标识类型 (仅个人理解)。

获取字段与函数签名的方式:

在.class的文件目录下 ,使用`javap -s -p className`   就可以列举出 , 所有的字段与方法签名
// 得到字段的值 , 类比java中的 对象.字段名得到值 , 这里是字段的ID
jstring jstr = (*env)->GetObjectField(env, jobj, jfID);

// 将jstring类型转换成字符指针 
char* cstr = (*env)->GetStringUTFChars(env, jstr, JNI_FALSE); 
//printf("is vaule:%s\n", cstr); 
// 拼接字符 char text[30] = " xiaojiu and "; strcat(text, cstr);
// 将字符指针转换成jstring类型 
jstring new_str = (*env)->NewStringUTF(env, text); 

因为java类型与C语言类型不是相通的 , 所有需要一个转换 , 类型的介绍在上一篇已经详细说明 。

// 将jstring类型的变量 , 设置到java 字段中 
// 类比java中的 对象.字段名得到值 , 这里是字段的ID
(*env)->SetObjectField(env, jobj, jfID, new_str);

画龙点睛:
上述中 , 我们访问修改了String类型的字段 , 也基本上能看出访问字段的基本套路 , 首先得到jclass , 再得到字段ID , 继而得到字段的值 , 进行类型转换 , 最后将变化的值设置给Java字段 。由此可以推出 , 访问其他类型的字段 , 也是这样的套路 , 只不过类型变了 , 值得注意的是 , java中的引用类型是需要进行类型转换的 。

part 2 : C函数访问Java方法

一 , 定义Java 方法与调用方法的native方法

// C语言调用java方法
private native void accessJavaRandomNumberMethod() ;

// 调用
jni.accessJavaRandomNumberMethod() ;

二 , 在C语言头文件中定义native方法的实现函数 , 并实现


// com.zeno.jni_HelloJNI.h
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaRandomNumberMethod
(JNIEnv *, jobject);


// Hello_JNI.c

// 访问java方法
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaRandomNumberMethod
(JNIEnv *env, jobject jobj) {

    // 得到jclass
    jclass jclazz = (*env)->GetObjectClass(env, jobj);
    // 得到方法ID
    jmethodID jmtdId = (*env)->GetMethodID(env, jclazz, "getRandomNumber", "(I)I");

    // 调用方法
    jint jRandomNum = (*env)->CallIntMethod(env, jobj, jmtdId, 10);

    // 打印
    printf("得到java方法的随机数:%ld\n", jRandomNum);

}

三 , 输出

得到java方法的随机数:6

四 , 分析

不论是字段访问还是方法的调用 , 其基本的套路不变 ,调用Java方法与调用字段不同的是 ,
将得到字段ID改成得到方法ID , 得到字段的值改成调用方法`CallXXX` , 通过调用调用Java方法得到方法返回值 。

part 3 : C函数访问Java字段与方法(静态)

套路都是一样的 , 这里仅给出代码 , 不做详细分析(本阶段全部代码) 。

/**
 * 
 * @author Zeno
 *
 *  JNI (Java Native Interface) java本地化接口
 *  
 *  Android Framework层与Native层相互通信的基石
 *  
 *
 */
public class HelloJni {
    
    // 使用C语言修改java字段
    public String name = "zeno" ;
    
    // 使用C语言修改java int 类型字段
    private int age = 20 ;
    
    public static String flag = "flag1" ;
    

    // 调用C语言函数方法
    public static native String getStringFromC() ;
    // 调用C++语言函数方法
    public static native String getStringFromCPP() ;
    
    // C语言修改java String 类型字段本地方法
    public native void accessJavaStringField() ;
    
    // C语言修改java String static 类型字段本地方法
    public native void accessJavaStaticStringField() ;
    
    // C语言修改java int 类型字段本地方法
    public native void accessJavaIntField() ;
    
    
    
    // C语言调用java方法
    private native void accessJavaRandomNumberMethod() ;
    
    // 调用Java静态方法
    private native void accessJavaStaticMethod() ;
    
    // 静态native方法访问字段
    private static native void staticAccessJavaField() ;
    
    public static void main(String[] args) {
        
        System.out.println("getStringFormC == "+getStringFromC());
        System.out.println("getStringFormC == "+getStringFromCPP());
        
        HelloJni jni = new HelloJni() ;
        System.out.println("修改前 name 的值:"+jni.name);
        //C语言修改java字段本地方法
        jni.accessJavaStringField();
        System.out.println("修改后 name 的值:"+jni.name);
        
        System.out.println("修改前 flag 的值:"+flag);
        //C语言修改java static 字段本地方法
        jni.accessJavaStaticStringField();
        
        System.out.println("修改后 flag 的值:"+flag);
        
        
        System.out.println("修改前 age 的值:"+jni.age);
        //C语言修改java字段本地方法
        jni.accessJavaIntField();
        
        System.out.println("修改后 age 的值:"+jni.age);
        
        jni.accessJavaRandomNumberMethod() ;
        
        jni.accessJavaStaticMethod() ;
        
        // 静态native方法 ,访问java字段
        
        System.out.println("修改前 flag 的值:"+flag);
         staticAccessJavaField();
         
         System.out.println("修改后 flag 的值:"+flag);
    }
    
    static{
        // 加载动态库
        System.loadLibrary("Hello_JNI") ;
    }
    
    
    // 调用java方法
    private int getRandomNumber(int bound) {    
        
        return new Random().nextInt(bound) ;
    }
    
    // 调用java静态方法
    private static String getUUID() {
        return UUID.randomUUID().toString();
    }
}

C实现 , 这里就不贴头文件了 。
调用静态的Java字段与方法 , 在C语言中调用相应的static函数 , 例如:获取静态字段ID GetStaticFieldID

#define _CRT_SECURE_NO_WARNINGS

#include "com_zeno_jni_HelloJni.h"

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

/*
    C/C++动态库 , 在win平台下以.dll文件标识 , 在linux下面以.so文件表示
    在Android中 , 以.so文件表示 , 因为Android使用的是linux内核 。

*/



/*
* Class:     com_zeno_jni_HelloJni
* Method:    getStringFormC
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_zeno_jni_HelloJni_getStringFromC
(JNIEnv *Env, jclass jclazz) {

    return (*Env)->NewStringUTF(Env, "Jni C String");
}

/*C语言访问java String类型字段*/
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *env, jobject jobj) {

    // 得到jclass , jclass就好比java的.class对象
    jclass jcls = (*env)->GetObjectClass(env, jobj); 

    // 得到字段ID , 
    jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");

    // 得到字段的值
    jstring jstr = (*env)->GetObjectField(env, jobj, jfID);

    // 将jstring类型转换成字符指针
    char* cstr = (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
    //printf("is vaule:%s\n", cstr);
    // 拼接字符
    char text[30] = "  xiaojiu and ";
    strcat(text, cstr);
    //printf("modify value %s\n", text);

    // 将字符指针转换成jstring类型
    jstring new_str = (*env)->NewStringUTF(env, text);

    // 将jstring类型的变量 , 设置到java 字段中
    (*env)->SetObjectField(env, jobj, jfID, new_str);
}

/*C语言访问java int 类型字段*/
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaIntField
(JNIEnv *env, jobject jobj) {

    // 得到jclass
    jclass jclazz = (*env)->GetObjectClass(env, jobj);

    // 得到字段ID
    jfieldID jfid = (*env)->GetFieldID(env, jclazz, "age", "I");

    // 得到字段值
    jint jAge = (*env)->GetIntField(env, jobj, jfid);

    jAge++;

    (*env)->SetIntField(env, jobj, jfid, jAge);

}

// 访问java方法
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaRandomNumberMethod
(JNIEnv *env, jobject jobj) {

    // 得到jclass
    jclass jclazz = (*env)->GetObjectClass(env, jobj);
    // 得到方法ID
    jmethodID jmtdId = (*env)->GetMethodID(env, jclazz, "getRandomNumber", "(I)I");

    // 调用方法
    jint jRandomNum = (*env)->CallIntMethod(env, jobj, jmtdId, 10);

    // 打印
    printf("得到java方法的随机数:%ld\n", jRandomNum);

}


// 访问java静态字段
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStaticStringField
(JNIEnv *env, jobject jobj) {

    // 得到jclass
    jclass jclazz = (*env)->GetObjectClass(env, jobj);
    // 得到字段ID
    jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");

    // 得到字段的值
    jobject jFLagStr = (*env)->GetStaticObjectField(env, jclazz, jfid);
    
    // 将java字符串转换成C字符指针
    char* cFlagStr = (*env)->GetStringUTFChars(env, jFLagStr, JNI_FALSE);

    //printf("is vaule:%s\n", cFlagStr);

    char newStr[30] = " access static field ";
    strcat(newStr, cFlagStr);

    // 将C字符指针 , 转换成java字符串
    jstring jNewStr = (*env)->NewStringUTF(env, newStr);

    // 将字符串设置到java字段上
    (*env)->SetStaticObjectField(env, jclazz, jfid, jNewStr);
}

// 访问java静态方法
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStaticMethod
(JNIEnv *env, jobject jobj) {

    // 得到jclass
    jclass jclazz = (*env)->GetObjectClass(env, jobj);

    // 得到静态方法ID
    jmethodID mtdid = (*env)->GetStaticMethodID(env, jclazz, "getUUID", "()Ljava/lang/String;");

    // 调用静态方法
    jobject jUUIDStr = (*env)->CallStaticObjectMethod(env, jclazz, mtdid);

    // 将java字符串转换成C字符指针
    char* cUUIDStr = (*env)->GetStringUTFChars(env, jUUIDStr, JNI_FALSE);

    printf("is vaule:%s\n", cUUIDStr);

    // 根据UUID生成临时文件
    char file_path[100] ;
    sprintf(file_path, "e:\\dn\\%s.txt", cUUIDStr);
    printf("is address:%s\n", file_path);

    FILE* fp = fopen(file_path, "w");
    if (fp == NULL) {
        printf("文件创建失败\n");
    }

    char* content = "落花有意流水无情";
    // 写入内容
    fputs(content, fp);

    // 关闭流
    fclose(fp);
}



// 静态native方法 , 访问java字段
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_staticAccessJavaField
(JNIEnv *env, jclass jclazz) {

    // 得到字段ID
    jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");

    // 得到字段的值
    jstring jflag = (*env)->GetStaticObjectField(env, jclazz, jfid);

    // 将java字符串转换成字符指针
    char* cXj = (*env)->GetStringUTFChars(env, jflag, JNI_FALSE);
    printf("is value:%s\n", cflag);
    char newName[100] = "xiaojiu love ";
    char* cNewName = strcat(newName, cflag);

    // 将字符指针转换成java类型
    jstring newStr = (*env)->NewStringUTF(env, cNewName);

    // 设置
    (*env)->SetStaticObjectField(env, jclazz, jfid, newStr);

}

编写套路

C语言访问Java语言的字段与方法 , 只要理解了一种 , 其他的都是套路 , 根据步骤一步一步来就可以了 。

步骤一 、 得到jclass , 字节码对象 , 如果是static native修饰 , 则函数会以jclass类型传入 , 非静态则需要得到jclass类型 。

// 得到jclass 
jclass jclazz = (*env)->GetObjectClass(env, jobj);

步骤二 、得到字段或方法ID , 区分静态字段与对象字段 , 静态字段或方法调用(*env)->GetStaticFieldID得到静态字段ID ,(*env)->GetStaticMethodID得到静态方法ID , 对象字段调用(*env)->GetFieldID得到字段ID,(*env)->GetMethodID得到方法ID 。 可以得到一个套路 , 静态修饰的 , 则调用static标识的函数 , 非静态的则调用常规函数 。

// 得到字段ID , 对象字段
 jfieldID jfid = (*env)->GetFieldID(env, jclazz, "age", "I");

// 得到字段ID , 静态字段
jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");

步骤三 、 取得字段的值或调用方法 , 需要注意的是, 得到字段的值与调用方法 , 都有类型的区分 。引用类型则使用GetObjectFieldCallStaticObjectMethod , 其他类型 , 则有对于的jxxx类型对应 。套路简写:Get<Type>FieldGetStatic<Type>FieldCall<Type>MethodCallStatic<Type>Method

// 得到字段的值 
jstring jstr = (*env)->GetObjectField(env, jobj, jfID);

// 得到字段值
 jint jAge = (*env)->GetIntField(env, jobj, jfid);

// 调用方法 
jint jRandomNum = (*env)->CallIntMethod(env, jobj, jmtdId, 10);

// 调用静态方法
 jobject jUUIDStr = (*env)->CallStaticObjectMethod(env, jclazz, mtdid);

步骤四 、 类型转换 , 如果是Java引用类型 , 则需要进行类型转换

// 将java字符串转换成字符指针
char* cflag = (*env)->GetStringUTFChars(env, jflag, JNI_FALSE);

结语

真正的高手 , 不是乐而学得的 , 真正的学习 , 不是轻轻松松的 。高手 , 需要刻意练习 , 刻意练习不是重复相同的动作 , 而是跳出舒适区熟悉区域 , 刻意练习自己不熟悉感觉艰难的事情 。感谢动脑学院

本文由老司机学院【动脑学院】特约提供。

做一家受人尊敬的企业,做一位令人尊敬的老师

参考资料:
Java Native Interface 6.0 Specification

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

推荐阅读更多精彩内容

  • 现在,你知道了如何通过JNI来访问JVM中的基本类型数据和字符串、数组这样的引用类型数据,下一步就是学习怎么样和J...
    738bc070cd74阅读 822评论 0 1
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 接续上篇JNI开发系列③C语言调用Java字段与方法 前情提要 Java调用C方法很简单 , 只需要编写nativ...
    逝我阅读 2,228评论 5 6
  • 什么是JNI? JNI 是java本地开发接口.JNI 是一个协议,这个协议用来沟通java代码和外部的本地代码(...
    a_tomcat阅读 2,767评论 0 54
  • 记得年初的一个晚上,跟秋同学聊天。两个单身青年,谈到对未来另一半的期待,秋同学blabla说了一大堆,我估摸了一下...
    文十言阅读 280评论 1 1