在JNI调用中,肯定会涉及到本地方法操作Java类中数据和方法。在Java1.0中“原始的”Java到C的绑定中,程序员可以直接访问对象数据域。然而,直接方法要求虚拟机暴露他们的内部数据布局,基于这个原因,JNI要求程序员通过特殊的JNI函数来获取和设置数据以及调用java方法。
一、取得代表属性和方法的jfieldID和jmethodID
为了在C/C++中表示属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID类型来分别代表Java端的属性和方法。我们在访问或是设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能在本地代码进行Java属性操作。同样的,我们需要调用Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java方法调用。
使用JNIEnv提供的JNI方法,我们就可以获得属性和方法相对应的jfieldID和jmethodID:
GetFieldID :取得成员变量的id
GetStaticFieldID :取得静态成员变量的id
GetMethodID :取得方法的id
GetStaticMethodID :取得静态方法的id
下面看这个方法的原型
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig) jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig) jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) jmethodID GetMethodID(jclass clazz, const char *name,const char *sig)可以看到这四个方法的参数列表都是一模一样的,下面来分析下每个参数的含义:
第一个参数jclass clazz :
上篇讲到jclass,相当于Java中的Class类,代表一个Java类,而这里面的代表的就是我们操作的Class类,我们要从这个类里面取的属性和方法的ID。
第二个参数const char *name
这是一个常量字符数组,代表我们要取得的方法名或者变量名。
第三个参数const char *sig
这也是一个常量字符数组,代表我们要取得的方法或变量的 签名。
什么是方法或者变量的签名呢?
看下面的例子:
下面的类中,有一个本地方法,和两个重载的show方法。
public class NativeTest { public native void showNative(); public void show(int i){ System.out.println(i); } public void show(double d){ System.out.println(d); } }那么,我们在本地方法中调用其中的某一个show方法:
//首先取得要调用的方法所在的类的Class对象,在C/C++中即jclass对象
jclass clazz_NativeTest=env->FindClass("cn/itcast/NativeTest");
//取得jmethodID
jmethodID id_show=env->GetMethodID(clazz_NativeTest,“show”,"???");
那这样取得的jmethodID到底是哪个show方法呢?所以这就是第三个参数sig的作用,sig其实是单词sign的缩写,它用于指定要取得的属性或方法的类型。
这里的sig如果指定为"(I)V",则返回void show(int)方法的jmethodID。如果指定为"(D)V",则返回void show(double)方法的jmethodID。
如何签名:
下面看看Sign签名如何写,来表示要取得的属性或方法的类型。
1、普通类型签名
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V
2、引用类型签名
object L开头,然后以/ 分隔包的完整类型,后面再加; 比如说String 签名就是 Ljava/lang/String;
Array 以[ 开头,在加上数组元素类型的签名 比如int[] 签名就是[I ,在比如int[][] 签名就是[[I ,object数组签名就是[Ljava/lang/Object;
3、方法签名
(参数1类型签名 参数2类型签名 参数3类型签名 .......)返回值类型签名
还要注意,就算java构造器没返回值,也加上V签名
由于签名比较难以记忆,JDK提供了一个工具javap来查看一个类的声明。其中就可以设置输出每个方法/属性的签名。
javap -s <options> className
-s 表示是签名
options 可以使-private -protected -public 用于选择性的输出private 或protected 或 public声明的方法/属性。
二、根据获取的ID,来取得和设置属性,以及调用方法。
取得了代表属性和方法的ID,就可以利用JNIEnv提供的方法来进行下一步的操作了。
1、如何获得和设置属性/静态属性
取得了代表属性和静态属性的jfieldID,就可以用在JNIEnv中提供的一系列的方法,来获取和设置属性/静态属性。
获取的形式如下:Get<Type>Field GetStatic<Type>Field。
设置的形式如下:Set<Type>Field SetStatic<Type>Field。
比如:取得属性:
jobject GetObjectField(jobject obj, jfieldID fieldID) jboolean GetBooleanField(jobject obj, jfieldID fieldID) jbyte GetByteField(jobject obj, jfieldID fieldID)
jobject GetStaticObjectField(jclass clazz, jfieldID fieldID) jboolean GetStaticBooleanField(jclass clazz, jfieldID fieldID) jbyte GetStaticByteField(jclass clazz, jfieldID fieldID)
Get方法的第一个参数代表要获取的属性的对象或者jclass对象,第二个参数即属性的ID
比如;设置属性:
void SetObjectField(jobject obj, jfieldID fieldID, jobject val) void SetBooleanField(jobject obj, jfieldID fieldID,jboolean val) void SetByteField(jobject obj, jfieldID fieldID, jbyte val)
比如;设置静态属性:
void SetStaticObjectField(jobject obj, jfieldID fieldID, jobject val) void SetStaticBooleanField(jobject obj, jfieldID fieldID,jboolean val) void SetStaticByteField(jobject obj, jfieldID fieldID, jbyte val)Set方法的第一个参数代表要设置的属性的对象或者jclass对象,第二个参数即属性的ID,第三个参数代表要设置的值。
2、如何调用方法
取得了代表方法和静态方法的jmethodID,就可以用在JNIEnv中提供的一系列的方法,来调用方法和静态方法。
有三种形式来调用方法和静态方法:
普通方法:
Call<Type>Method(jobject obj, jmethodID methodID,...) Call<Type>MethodV(jobject obj, jmethodID methodID,va_list args) Call<Type>tMethodA(jobject obj, jmethodID methodID,const jvalue *args)静态方法:
CallStatic<Type>Method(jclass clazz, jmethodID methodID,...) CallStatic<Type>MethodV(jclass clazz, jmethodID methodID,va_list args) CallStatic<Type>tMethodA(jclass clazz, jmethodID methodID,const jvalue *args)这里面的Type也和上面获取和设置属性的Type一样,表示各种类型,如Int,Char,Byte等等,这里面的Type代表的是这个方法的返回值类型。
第一个参数代表调用的这个方法属于哪个对象,或者这个静态方法属于哪个类。
第二个参数代表jmethodID。
后面的参数,就代表这个方法的参数列表了。只不过有3中方式来设置这个参数列表。
下面来详细说明如何通过这3个方法来设置参数列表(以非静态方法举例,静态方法和的设置一致)。
方式1、Call<Type>Method(jobject obj, jmethodID methodID,...)
用不定参数的形式来一个个指定对应的参数列表。比如有这么一个Java方法
public int show(int i,double d,char c){ 。。。。 }通过这个方法,要这样设置参数列表:
jint i=10L; jdouble d=2.4; jchar c=L'd'; env->CallIntMethod(obj,id_show,i,d,c);
这种方式使用很少。
方式三:Call<Type>MethodA(jobject obj, jmethodID methodID,jvalue* v)
第三种传参的形式是传入一个jvalue的指针。jvalue类型是在 jni.h头文件中定义的联合体union,看它的定义:
typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue;联合体可以储存不同类型的变量,但是一个联合体对象只能存储它里面定义的某一个类型的变量。
例子:
jvalue * args=new jvalue[3]; args[0].i=12L; args[1].d=1.2; args[2].c=L'c'; jmethodID id_goo=env->GetMethodID(env->GetObjectClass(obj),"goo","(IDC)V"); env->CallVoidMethodA(obj,id_goo,args); delete [] args; //释放内存
三、如何调用子类重写的父类方法
在Java::
class Father{ public void show(){ System.out.println("Father"); } } class Son extends Father{ @Override public void show() { System.out.println("Son"); } }
public static void main(String[] args) { Father father=new Son(); father.show(); }这里调用的是Son的show方法,而不是Father的show方法,这就是java的多态。
但是在C++中,
class Father{ public: void show(){ cout<<"Father"<<endl; } }; class Son:public Father{ public : void show(){ cout<<"Son"<<endl; } };
Father* father=new Son(); father->show();
这是因为C++和Java的多态的不一样的地方,只有当一个父类的方法被声明为virtual——虚函数的时候,才能被子类覆盖,才能实现多态。
而Java默认实现了这一点,Java类中的所有成员方法都是虚函数。所以能够直接实现多态,但是C++中的不同,必须我们自己加上关键字virtual。
所以,如果把上面的C++代码改成如下形式,就可以实现和Java一样的效果:
class Father{ public: virtual void function(){ cout<<"Father"<<endl; } }; class Son:public Father{ public : void function(){ cout<<"Son"<<endl; } };
那么,在JNI中,我们可不可以通过子类的对象,去调用父类被子类重写的方法呢?我们知道,在Java中是不可以的,最多只能在重写该方法的时候,使用super调用一下,但是在其他的地方就可以调用了。但是我们在JNI中,却可以实现子类调用父类被重写的方法。 这也是为了让JNI的本地方法更加贴近C/C++的特性。
要想实现子类调用父类被重写的方法,要使用JNIEnv提供的一系列方法。形式如下:
CallNonvirtual<Type>Method(jobject obj, jclass clazz,jmethodID methodID, ...)
CallNonvirtual<Type>MethodV(jobject obj, jclass clazz, jmethodID methodID, va_list args)
CallNonvirtual<Type>MethodA(jobject obj, jclass clazz,jmethodID methodID, const jvalue * args)
Type就是表示这个方法的返回类型,如Object、Boolean、Int。再看方法参数的含义:
第一个参数 jobject obj 代表的子类的对象
第二个参数jclass clazz 代表的是父类的jclass对象
第三个参数jmethodID methodID 代表的是父类中 被子类重写的方法的ID(也就是父类方法的ID,而不是子类的)。
后面的参数都是指定方法的参数列表,这和上面讲到的方法调用一样的。
下面看看使用实例:
有这样一段Java代码。
package com.tao.test; public class Test { private Son son=new Son(); public native void show(); static{ System.loadLibrary("NativeTest"); } public static void main(String[] args) { new Test().show(); } } class Father{ public void function(){ System.out.println("Father"); } } class Son extends Father{ @Override public void function() { System.out.println("Son"); } }
下面,我们来书写我们的本地方法show。
JNIEXPORT void JNICALL Java_com_tao_test_Test_show (JNIEnv * env, jobject obj) { //首先取得Test类中son属性的ID jfieldID id_son=env->GetFieldID(env->GetObjectClass(obj),"son","Lcom/tao/test/Son;"); //取得son属性 jobject son=env->GetObjectField(obj,id_son); //先看调用子类的show方法 //取得子类的show方法的id,然后调用子类的show方法 jmethodID id_show=env->GetMethodID(env->GetObjectClass(son),"function","()V"); env->CallVoidMethod(son,id_show); //再看如果调用父类中被重写的方法 //取得父类Father的jclass对象 jclass clazz_father=env->FindClass("com/tao/test/Father") //取得父类中show方法的id jmethodID id_show_father=env->GetMethodID(clazz_father,"function","()V"); //调用CallNonvirtual<Type>Method方法调用父类中被重写的方法 env->CallNonvirtualVoidMethod(son,env->FindClass("com/tao/test/Father"),id_show_father); }
Son
Father