昨天,我写了一下JNI的基本组成和功能。今天继续完善以下几个部分
0:关于实例引用和类引用(昨天遗留的关于第二个参数的问题)
其实这个有点想太多了。今天验证了一下,写了两个不同的native方法。
//Demo.java
class Demo { //略... private static native void StaticMethod(String userInput);//静态方法 private native void InstaceMethod();//实例方法
public static void main(String args[]) { //省略... } }
对应编译出来的头文件是这样的
//略... JNIEXPORT void JNICALL Java_Demo_StaticMethod (JNIEnv *, jclass, jstring);
//...
JNIEXPORT void JNICALL Java_Demo_InstaceMethod (JNIEnv *, jobject); //略...
一些简单的例子都不会用到jclass,但是一旦有用到静态的方法去操作一些东西,就会用到jclass这个类引用了。他们唯独的区别其实也就在这了。
1:JNI的传参
在上一篇博文里已经提到了JAVA层的对象传递给了JNI层。它是通过一个C指针类型,指向了JAVA VM的内部结构,因此千万不可以为了效率或者方便起见而绕过JNI的内部函数直接操作这个指针。同时JNI中的前面两个参数也在上篇文章已经介绍过,如果涉及到在native方法内传递参数,那么就涉及到第三个参数了。比如需要传递一个字符串,那么第三个参数就会是jstring。这里的jstring是一种类型。在JNI中,数据类型基本上可以这么认为,在原有的大家所熟悉的数据类型前面加了j。比如int类型为jint、boolean为jboolean。对应的方法也一应俱全,相信老前辈们已经将它完善的很好了。况且如今IDE这么强大(我目前都是用sublime写java,命令行编译,更别说别的IDE了),再加上网络。用多了自然就熟悉啦。下面举个小例子介绍一下。
1.1字符串
先看java代码:
//Prompt.java
class Prompt { static { System.loadLibrary("Prompt");//提示加载名字为Prompt.dll库 } private native String getLine(String prompt);//声明native方法 public static void main(String argv[]) { Prompt p = new Prompt(); String input = p.getLine("Type a line:");//字符串作为参数 System.out.println("User typed:" + input); } }
//Prompt.c
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <jni.h> #include "Prompt.h" JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { char buf[128]; const jbyte *str; //将字符串转换成UTF-8 str = (*env)->GetStringUTFChars(env, prompt, NULL); if (str == NULL) { return NULL; } printf("%s", str); //释放转换时候的内存 (*env)->ReleaseStringUTFChars(env, prompt, str); scanf("%s", buf); //构建一个新的字符串返回 return (*env)->NewStringUTF(env, buf); }
如果直接打印printf("%s", prompt);jstring和char*是不同类型。需要转换成C能识别的char*,JNI支持Unicode/UTF-8字符编码互转的。这里的 几个方法要记住,字符串操作很普遍哦。这里看记得调用ReleaseStringUTFChars释放GetStringUTFChars中分配的内存。其中“str = (*env)->GetStringUTFChars(env, prompt, NULL);”中第三个参数原型是jboolean *。为JNI_TRUE会返回一个拷贝,不关心的时一般设置为NULL。因此这里不确定是否会分配内存。由于GC(垃圾回收机制)的原因,这里可以理解是回收内存,或者回收了一个内存占用标记。如果是后者,那么string的内存呢暂时未被及时回收,并且又有大量的操作,这样就很有可能导致程序崩溃,这是不安全的,所以要及时释放内存。这里也体现了一点,JNI让JAVA丢失了一部分安全性,需要程序员自己去考虑了。
1.2数组
这里代码就不贴了,上一篇文章中有一段关于二维数组的代码。
数组是由基本数据类型构成的集合,先调用FindClass获得一个一维某类型数组。可以调用NewObjectArray分配一个对象数组。也是Stirng的操作方式。
数组类型就不再是基本数据类型的传递了,它大致和下面要说的类成员和方法的访问方式类似。这里不多说了。
2:类的成员和方法的访问
2.1成员的访问方式
有静态成员和实例成员两种访问方式。其实过程都一样,这里贴出静态成员访问方法,实例成员只要把实现方法中的static去掉就可以了,其实这都是VS会自动提示你的。只管放心大胆的用。
//访问类的静态成员 JNIEXPORT void JNICALL Java_StaticField_accessField (JNIEnv *env, jobject obj) { jclass jcls; jint num; jfieldID jfid; //得到jclass jcls = (*env)->GetObjectClass(env, obj);
//GetObjectClass和FindClass一样,都能得到jclass。前者需要jni传入的一个引用,后者需要完整类名 if (NULL == jcls) { return; } //得到jfield jfid = (*env)->GetStaticFieldID(env, jcls, "num", "I");//和取得实例成员ID的不同的是加了static if (NULL == jfid) { return; } //得到成员值 num = (*env)->GetStaticIntField(env, jcls, jfid); printf("in C: "); printf("StaticField.num = %d ", num); //构建并修改值 (*env)->SetStaticIntField(env, jcls, jfid, 9999); }
2.2类方法的访问方式
JNIEXPORT void JNICALL Java_StaticMethod_StaticMethod (JNIEnv *env, jobject obj) { jclass jcls; jmethodID jmid; jcls = (*env)->GetObjectClass(env, obj); if (NULL == jcls) { return; } jmid = (*env)->GetStaticMethodID(env,jcls,"callback","()V"); if (NULL == jmid) { return; } //改进方法 static jmethodID jmid_s; if (NULL == jmid_s) { jmid_s = (*env)->GetStaticMethodID(env, jcls, "callback", "()V"); if (NULL == jmid_s) { return; } } //再将下边的jmid改成jmid_s,这样便可以实现加载一次之后,知道被卸载都不再对名称和符号表进行查询操作 //可以将初始化的field和method一起缓存。这样再次装载就会更新值。 //改进二:可以首次加载的时候将一个类的所有field和method的信息加载进Hash表。 //以后的操作只要最这个表进行操作就OK了。用名字和类型做键值 printf("In C call the Java instance method: ------------- "); (*env)->CallStaticVoidMethod(env, jcls, jmid); }
2.3 访问优化方式
见变代码中的注释。同样可以类推到其他方式总
3:关于全局引用和局部引用
JNI支持3种引用,全局引用,局部引用,弱全局引用。
全局引用:和全部变量差不多,和程序共存亡,必须通过NewGlobalRef由程序员主动创建。
局部引用:每创建一个实例就会返回一个指向这个实例的局部引用,只在本线程中native方法中有效。当native 返回时会自动释放。
弱全局引用 特性与GlobalRef相同,但该类型的引用不保证不被自动回收