浅谈JNI(二)

浅谈JNI中域、方法、对象的创建以及数组

Posted by Anriku on May 4, 2018

在上一篇JNI的博客中,我们一起对JNI有了个初步的了解,学会了基本的JNI编写。这篇博客我们将了解JNI中如何使用JNI中域、方法、对象的创建以及数组。前面已经介绍了C和C++的不同之处,虽然C++来写会简单很多,但是考虑到会C的更多一些,所以这篇博客中我会默认使用C。话不多说,咱们开始吧!开始我们进行基本的一下讲解的时候可能不太明白,但这没关系,最后我们会有一个具体的实例让大家更加深刻地进行理解学习!

访问以及修改域

实例域

JNI中访问或修改Java对象的实例域有如下几步:

  • 首先,获取类引用。也就是对应的jclass对象
//方法原型
GetObectClass(JNI *env,jobject obj);

//获取对象的类的实例
jclass class_String = (*env)->GetObjectClass(env,obj_String);
  • 然后,我们就准备获取对应的实例域了?Too young to simple!!!这里我们还得获取用于标识结构中的域的ID(是一个jfieldID类型)。下面的方法原型中最后一个叫做编码签名我们稍后介绍。
//方法原型
GetFieldID(JNIEnv *env,jclass cl,const char name[],const char fieldSignature[]);

//获取fieldID
jfieldID id_out = (*env)->GetFieldID(env,class_System,"out","Ljava/lang/String;")
  • 最后,我们才进行对象域的访问以及修改。栗子我就不给了这个要有具体的类来说明才行,最后综合代码我们再一起看。
//方法原型 (其中,Xxx表示Java类型)
GetXxxField(JNIEnv *env,jobject obj,jfeildID feild_id);
SetXxxField(JNIEnv *env,jobject obj,jfeildID feild_id,Xxx val);

静态域

上面说了实例域的访问和修改。其实静态域差不多。只是加上Static就行

下面我们只介绍有差别的地方:

  • 获取类引用的方式有些不同。由于我们没有类的对象因此不能用GetObjectClass,只能用FindClass(JNI *env,const char className[])来获取
  • 然后就是最后我们进行静态域的访问和修改不同。之前实例域我们第二个参数是jobject对象,现在是对应的类引用。
//方法原型
GetStaticXxxFeild(JNIEnv *env,jclass cl,jfeildID feild_id);
SetStaticXxxFeild(JNIEnv *env,jclass cl,jfeildID feild_id,Xxx val)

编码签名

前面我们提到了编码签名。其实编码签名就是将数据类型的名称和方法签名进行“混编”的规则(方法签名描述方法的参数和返回值的类型)。换句话说,编码签名就是对不同的类型换一个标识。下面是编码签名和不同类型的对应关系:

编码签名 Java数据类型/方法签名
B byte
C char
D double
F float
I int
J long
L<className>; 对应的类的类型
S short
V void
Z boolean
[<对应的类型包括基本类型> / [<对应的类的类型>; 一维数组
[[<对应的类型包括基本类型> / [[<对应的类的类型>; 二维数组
(参数列表)返回值 方法签名

上面是编码签名的对应规则。那么我得特别注意的是在类的后面才有分号,其它都没有分号。下面来个栗子更深刻的理解。

//一个方法
void get(double a,double b,String c);

//对应的编码签名,观察前两个D,发现了什么
(DDLjava/lang/String;)V

但其实上面所说的编码规则你可以完全不用记的。如果你想看某个类的所有的域、方法的编码签名。你可以用下面的命令。

javap -s -private <对应的class名>

下面是一个截图:

编码签名

方法的调用

其实嘛。方法的整个调用的流程和域的访问是大同小异的。还是三大步:

  • 获取类引用。也就是jclass对象
  • 获取方法ID。前面的域中我们是获取的域ID
  • 进行方法的调用

由于基本相同我就不同进行详细的介绍了。只是将一些API写出来。

//获取类引用一样的就不说了

//获取方法ID
jmethodID GetMethodID(JNIEnv *env,jclass cl,const char name[],const char methodSignatue[]);

jmethodID GetStaticMethodID(JNIEnv *env,jclass cl,const char name[],const char methodSignatue[]);

//调用方法
Xxx CallXxxMethod(JNIEnv *env,jobject obj,jmethodID id,args...);

Xxx CallXxxMethod(JNIEnv *env,jclass cl,jmethodID id,args...);

其它的API还有很多我就不一一列出了这里列出一些常用的。具体的参考官网的API

当然要调用构造器的整个步骤是一样的。但是由于有一些调用的不同。下面将一些不同进行单独地说明:

  • 获取构造器的ID的时候。方法是完全一样的只不过方法名我们填<init>。而且构造器的返回值是void,因此填编码签名的时候要特别注意。下面给个栗子吧!
//获取FileOutputStream的构造器ID
jmethodID id_FileOutputStream = (*env)->GetMethodID(env,class_FileOutputStream,"<init>","(Ljava/lang/String;)V");
  • 创建对象
//方法原型
NewObject(JNIEnv *env,jclass cl,jmethodID id,args...);

访问和修改数组

关于Java和C数组有如下的对应关系:

Java数组类型 C数组类型
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
int[] jintArray
short[] jshortArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
Object[] jobjectArray

这里有一点要提的就是:在C中所有的这些数组其实是jobject的同义类型;而在C++中它们有如下的继承关系:

C++中数组继承层次

下面列出常用的API。更多的API仍然访问官网啦!

//获取数组的元素的个数(jsize就是jint)
jsize GetArrayLength(JNI *env,jarray array);

//1.当数组为对象数组时:
//获取数组对应位置的元素
jobject GetObjectArrayElement(JNIEnv *env,jobjectArray array,jsize index);

//设置数组对应位置的元素
void SetObjectArrayElement(JNIEnv *env,jobjectArray array,jsize index,jobject val);



//2.当数组为基本类型数组时
//获取数组首地址
Xxx* GetXxxArrayElements(JNIEnv *env,jarray array,jboolean *isCopy);

//通知虚拟机通过上面方法获取的指针不需要了
void RealeaseXxxArrayElements(JNIEnv *env,jarray array,Xxx elems[],jint mode);

上面的方法很简单,我们分别对获取和修改元素做一个说明吧:

  • 当数组元素是对象的时候,我们使用GetObjectArrayElement和SetObjectArrayElement来进行元素的访问及修改。
  • 当数组为基本类型的时候,我们使用下面两个方法。
    • GetXxxArrayElements方法中Xxx是指任意的基本类型,然后这个方法会返回一个数组的首地址,还需要注意的是通过这个方法我们也许会对原数组进行复制,那么我们如何知道是否复制了原数组呢?最后一个参数传入一个jboolean指针进去如果返回JNI_TRUE那么数组就复制了,返回JNI_FALSE就没有复制。当复制后返回的指针肯定是指向的复制后的数组。那么我们如果要将数组的改变给原数组的话,我们就需要调用下面的方法了。而且,在使用
    • ReleaseXxxArrayElements方法一个作用就是通知虚拟机通过上面方法获取的指针不需要了。然后就是如果上面一个方法调用的时候没有复制数组的话,这个参数是没有用的;如果复制了,最后一个参数有下面的对应关系:
mode action
0 把复制的数组所做的修改同步到原有的数组中,并且把复制的数组进行释放
JNI_COMMIT 把复制的数组所做的修改同步到原有的数组中,但是不把复制的数组释放调用
JNI_ABORT 把复制的数组释放掉,但是不把复制数组的改变同步到原数组去

其中需要注意一点的就是,我们即使是将复制的数组释放掉后,数组的内容还是可以访问而且还是原来的值。只不过这个地方已经被标记为了未使用的状态。一般情况下直接填0就好。

具体实例

前面我们在概念上进行了简单的介绍。下面我们来一个具体的实例:

首先,我们来看一下JNITest这个Java类,在这个类中我们进行了Complex这个Java类的使用。具体的两个使用在下面都有注意。其中,Complex类是一个表示复数的类。

public class JNITest{
	public static void main(String[] args){
		//进行多个复数的相加
		Complex[] comps = new Complex[2];
		Complex a = new Complex(2,2);
		Complex b = new Complex(2,2);
		Complex c = new Complex(4,4);
		comps[0] = b;
		comps[1] = c;
		Complex result = a.addTogether(comps);
		System.out.println("real is " + result.getReal() + ", imag is " + result.getImag());
	

		//进行数组的修改
		int[] array_c = {0,1,2,3};
		a.modifyArray(array_c);
		System.out.println("The Modified Result:");
		for (int i = 0;i < 4;i++ ) {
			System.out.println(array_c[i]);
		}

	}
}

然后,我们来深入Complex这个类的实现:

public class Complex{
    //real表示实部,imag表示虚部
	private double real;
	private double imag;

	public Complex(double real,double imag){
		this.real = real;
		this.imag = imag;
	}

	public void setReal(double real){
		this.real = real;
	}

	public double getReal(){
		return real;
	}

	public void setImag(double imag){
		this.imag = imag;
	}

	public double getImag(){
		return imag;
	}

    //这个本地方法是将多个复数相加
	public native Complex addTogether(Complex[] comps);

    //这个本地方法是将传入的Java数组进行修改
	public native void modifyArray(int[] a);

	static{
		System.loadLibrary("complex");
	}
}

之后,我们通过前面一篇博客的讲解的过程获取含有上面两个本地方法的头文件。并进行方法的具体实现。实现如下:

#include "Complex.h"
#include "stdlib.h"


JNIEXPORT jobject JNICALL Java_Complex_addTogether
(JNIEnv *env, jobject obj, jobject comps){
    int i;
    jdouble real;
    jdouble imag;
    
    //获取class
    jclass class_Complex = (*env)->GetObjectClass(env,obj);
    
	//获取real域的fieldID
	jfieldID id_real = (*env)->GetFieldID(env,class_Complex,"real","D");
    //获得imag域的fieldID
	jfieldID id_imag = (*env)->GetFieldID(env,class_Complex,"imag","D");
    //获取构造器的fieldID
    jmethodID id_constructor = (*env)->GetMethodID(env,class_Complex,"<init>","(DD)V");
    
    real = (*env)->GetDoubleField(env,obj,id_real);
    imag = (*env)->GetDoubleField(env,obj,id_imag);
    
    jobjectArray array_comps = comps;
    
    //第一个对象我们取出来进行修改
    jobject first_obj = (*env)->GetObjectArrayElement(env,array_comps,0);
    (*env)->SetDoubleField(env,first_obj,id_real,20);
    (*env)->SetDoubleField(env,first_obj,id_imag,20);
    
    for ( i = 1;i < (*env)->GetArrayLength(env,array_comps);i++){
        real += (*env)->GetDoubleField(env,(*env)->GetObjectArrayElement(env,array_comps,i),id_real);
        imag += (*env)->GetDoubleField(env,(*env)->GetObjectArrayElement(env,array_comps,i),id_imag);
    }
    
    //构造结果Complex
    jobject result = (*env)->NewObject(env,class_Complex,id_constructor,real,imag);

    return result;
}


JNIEXPORT void JNICALL Java_Complex_modifyArray
(JNIEnv *env, jobject obj, jintArray arr_in){
    jboolean *isCopy;
    int i;
    jint *in;
    in = (*env)->GetIntArrayElements(env,arr_in,isCopy);
    printf("%d\n", *isCopy);
    for (i = 0;i < (*env)->GetArrayLength(env,arr_in);i++){
        in[i] = i*10;
    }
    (*env)->ReleaseIntArrayElements(env,arr_in,in,0);
}

这里面的具体的东西我就不再讲了,大家可以根据今天的讲解去具体的分析。

最后,就是编译动态链接库进行运行了。不清楚的仍然是可以去看上一篇博客。

下面怕大家可能还是有点不清楚具体的文件架构,我把所有的文件截一个图:

JNI(二)

需要注意的是这里的动态库是macOS上的动态库,根据具体的操作系统的不同动态库也会是不同的。

下面是运行结果的截图:

运行结果

总结

今天我们主要对域的访问和修改、方法的调用以及数组的使用进行了讲解。东西可能有点多,但是域的访问和修改、方法的调用都是大同小异的。都是那三大步,这里就不重复。对比地来进行学习会更加地容易。

参考

《Java核心技术 卷二》

官方文档

转载请注明链接