转载请注明出处:【huachao1001的:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】
本文主要针对C代码中访问JVM中对象的普通变量、静态属性、普通函数、静态函数进行举例讲解,通过本文的学习将进一步理解JNIEnv在本地代码和Java之间的重要性。有了前面几篇文章的基础,学习起本文来将更容易。好了,接下来往下学习吧~。
1. 访问实例对象的属性
下面实现一个简单的功能:在本地C代码中,修改Java类实例的属性值。首先在Java类中定义2个简单的属性,代码如下:
package com.huachao.java;
/**
* Created by HuaChao on 2017/03/22.
*/
public class HelloJNI {
private int number = 88;
private String message = "Hello from Java";
static {
System.loadLibrary("HelloJNI");
}
private native void modifyField();
public static void main(String[] args) {
HelloJNI obj = new HelloJNI();
obj.modifyField();
System.out.println("Java类中,number的值为:" + obj.number);
System.out.println("Java类中,message属性为:" + obj.message);
}
}
上面代码很简单,就是定义了一个本地函数modifyField(),还有两个属性number和message。接下来,生成c代码的头文件后,c代码对应的函数代码如下:
#include
#include
#include "com_huachao_java_HelloJNI.h"
JNIEXPORT void JNICALL Java_com_huachao_java_HelloJNI_modifyField
(JNIEnv * env,jobject thisObj){
//获取实例对象类的引用
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
// 获取实例对象的nunber对应的属性ID,int类型
jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I");
if (NULL == fidNumber) return;
//获取实例对象中对应的属性的值
jint number = (*env)->GetIntField(env, thisObj, fidNumber);
printf("在C代码中,number属性值为:%d\n", number);
// 修改number的值
number = 99;
(*env)->SetIntField(env, thisObj, fidNumber, number);
// 同理,获取类对象中的message的属性ID
jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;");
if (NULL == fidMessage) return;
// 获取给定属性ID的属性对象
jstring message = (*env)->GetObjectField(env, thisObj, fidMessage);
// 创建C语言中的字符串
const char *cStr = (*env)->GetStringUTFChars(env, message, NULL);
if (NULL == cStr) return;
printf("在C代码中, 字符串message为:%s\n", cStr);
(*env)->ReleaseStringUTFChars(env, message, cStr);
// 创建新的C字符串,并赋值给实例对象的属性
message = (*env)->NewStringUTF(env, "Hello from C");
if (NULL == message) return;
// 修改实例对象的属性值
(*env)->SetObjectField(env, thisObj, fidMessage, message);
}
编译为dll文件后,运行结果如下:
Java类中,number的值为:99
Java类中,message属性为:Hello from C
在C代码中,number属性值为:88
在C代码中, 字符串message为:Hello from Java
看到运行结果觉得挺奇怪,因为先调用modifyField函数,在打印结果中先打印了Java中的代码后打印C中的代码。现在我也还不清楚具体原因,但从结果上看,是先执行本地代码的。因此打印的先后顺序暂时可不管他,我目前认为是c的输出流和Java的输出流在输出到IntelliJ的控制台时合并的先后顺序造成的吧,如果有小伙伴知道原因,可以在评论中指出。
下面对代码进行解释,访问实例对象的属性可分为如下步骤:
- 首先是通过
jobject
指代的实例对象获取实例对象在Java虚拟机中对应的Class
对象,即对应于jclass
类型。这一步通过调用JNIEnv
对象的jclass GetObjectClass(JNIEnv *env, jobject obj);
函数来完成。- 接下来,获取用于标识属性的ID对象,即
jfieldID
类型对象。通过调用JNIEnv
的jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
函数实现。GetFieldID函数前两个参数不用解释,第三个参数是表示Java类中属性的名称,第四个参数表示属性的描述符。关于描述符在《IntelliJ IDEA平台下JNI编程(二)—类型映射》已经做过介绍,不清楚的可以回去查阅。- 接下来是根据属性的ID获取属性的值,通过调用
JNIEnv
的NativeType Get
来实现。此时可获取Java实例对象的的属性。Field(JNIEnv *env, jobject obj, jfieldID fieldID); - 如果需要对实例对象的属性进行修改,调用
JNIEnv
的void Set
实现。Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);
可以看到,在C代码中访问Java实例对象的属性与Java中通过反射的方法访问非常类似。只是这里用到了ID的方式来进行访问,而不是Java反射中的Field对象来访问。这主要是在跨越两种语言相互访问内存时,通过唯一标识码来间接访问效率更高也更简单。
2. 访问类的静态变量
因为静态变量是属于类对象,即Class对象。因此在访问静态变量时,c代码获取到对应的jclass对象后,即可访问。下面通过一个简单例子说明,首先在HelloJNI.java类中添加一个静态变量。
private static double price = 55.66;
在c代码的Java_com_huachao_java_HelloJNI_modifyField
中,代码如下:
JNIEXPORT void JNICALL Java_com_huachao_java_HelloJNI_modifyField
(JNIEnv * env,jobject thisObj){
// 获取实例对象类的引用
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
// 获取静态变量,并修改静态变量的值
jfieldID fidNumber = (*env)->GetStaticFieldID(env, thisClass, "price", "D");
if (NULL == fidNumber) return;
jdouble number = (*env)->GetStaticDoubleField(env, thisClass, fidNumber);
printf("In C, the double is %f\n", number);
number = 77.88;
(*env)->SetStaticDoubleField(env, thisClass, fidNumber, number);
}
运行结果如下:
Java类中,price的值为:77.88
In C, the double is 55.660000
静态变量的访问过程跟实例变量类似,只是对应的函数多了static部分,这里不再继续解释。
3. 调用Java的对象方法和静态方法
调用Java的函数过程与访问Java属性过程一致,有了前面的经验后,学习起调用函数来更容易了。下面同样通过一个简单例子来学习。首先在Java类中,定义3个具有代表性的函数:有返回值的对象方法、无返回值的对象方法,以及有返回值的静态方法。
package com.huachao.java;
/**
* Created by HuaChao on 2017/03/22.
*/
public class HelloJNI {
static {
System.loadLibrary("HelloJNI");
}
private native void nativeMethod();
private int sum(int n1, int n2) {
return n1 + n2;
}
private static double avg(int n1, int n2) {
return (double) ((n1 + n2) * 1.0f / 2);
}
private void display() {
System.out.println("invoke display()");
}
public static void main(String[] args) {
HelloJNI obj = new HelloJNI();
obj.nativeMethod();
}
}
在c代码中,同样是获取方法的id后,再调用方法。
JNIEXPORT void JNICALL Java_com_huachao_java_HelloJNI_nativeMethod
(JNIEnv * env,jobject thisObj){
// 获取实例对象类的引用
jclass thisClass = (*env)->GetObjectClass(env, thisObj);
//获取方法ID,注意普通函数和静态函数的获取区别
jmethodID sumId = (*env)->GetMethodID(env, thisClass, "sum", "(II)I");
jmethodID avgId = (*env)->GetStaticMethodID(env, thisClass, "avg", "(II)D");
jmethodID displayId = (*env)->GetMethodID(env, thisClass, "display", "()V");
if (NULL == sumId) return;
if (NULL == avgId) return;
if (NULL == displayId) return;
//调用方法,注意调用静态方程和普通方法的区别
(*env)->CallVoidMethod(env, thisObj, displayId);
double avgRs=(*env)->CallStaticDoubleMethod(env,thisClass,avgId,1,3);
printf("avg(1,3)=%f\n",avgRs);
int sumRs=(*env)->CallIntMethod(env,thisObj,sumId,1,3);
printf("sum(1,3)=%d\n",sumRs);
}
运行结果如下:
invoke display()
avg(1,3)=2.000000
sum(1,3)=4
代码中解释很清楚了,这里就不再解释。
4. 调用父类中被覆盖的方法
构造一个简单的继承关系:
package com.huachao.java;
/**
* Created by HuaChao on 2017/03/22.
*/
class Parent {
protected void sayHello() {
System.out.println("Hello in Parent");
}
}
public class HelloJNI extends Parent{
static {
System.loadLibrary("HelloJNI");
}
private void sayHello() {
System.out.println("Hello in Child");
}
private native void nativeMethod();
public static void main(String[] args) {
HelloJNI obj = new HelloJNI();
obj.nativeMethod();
}
}
在c代码中,通过子类的实例调用父类的被覆盖的函数方法如下:
JNIEXPORT void JNICALL Java_com_huachao_java_HelloJNI_nativeMethod
(JNIEnv * env,jobject thisObj){
// 在JVM中查找指定的Class对象
jclass parentClass=(*env)->FindClass( env,"com/huachao/java/Parent");
jmethodID sayHelloId = (*env)->GetMethodID(env, parentClass, "sayHello", "()V");
if (NULL == sayHelloId) return;
(*env)->CallNonvirtualVoidMethod(env, thisObj, parentClass,sayHelloId);
}
运行结果如下:
Hello in Parent
注意GetMethodID函数,因为传入的是com/huachao/java/Parent
对应的Class对象,因此返回的ID是指com/huachao/java/Parent
类中的sayHello
函数。调用父类的函数通过CallNonvirtual
来实现。CallNonvirtual
函数有如下几种原型:
NativeType CallNonvirtualMethod(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtualMethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtualMethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);
学到这里会发现,c代码中要想访问JVM中对象,必须通过JNIEnv
提供的函数来间接完成。这也进一步加深了对JNIEnv
的了解。