key_input *cpp_obj = (key_input*)peer; env->DeleteGlobalRef(cpp_obj->back_ptr); delete cpp_obj; return; }
本地方法create生成一个C++结构key_input,并初始化back_ptr字段。其中back_ptr是一个全局引用,指向KeyInput这个peer class对象的实例。本地方法destroy删除指向KeyInput对象的引用和KeyInput指向的本地数据结构。KeyInput构造方法调用本地方法create来建立KeyInput这个对象实例和它的副本key_input这个本地数据结构之间的链接。
Key_input::key_pressed(int key)方法的实现如下:
// returns 0 on success, -1 on failure int key_input::key_pressed(int key) {
jboolean has_exception;
JNIEnv *env = JNU_GetEnv(); JNU_CallMethodByName(env,
&has_exception, java_peer, \ \ key); if (has_exception) {
env->ExceptionClear(); return -1; } else {
return 0; } }
本节结束之间,我们还有最后一个话题需要讨论。假设,你为KeyInput类添加了一个finalize方法来避免内存泄漏。
class KeyInput { ...
public synchronized destroy() { if (peer != 0) { destroy(peer); peer = 0;
} }
protect void finalize() { destroy(); } }
考虑到多线程的情况,destroy方法被加上了synchronized关键字。但是,上面的代码不会像你期望的那样执行的,因为JVM永远不会回收KeyInput这个对象,除非你手动调用destory方法。因为,KeyInput的构造方法创建了一个到KeyInput对象的JNI全局引用,这个全局引用会阻止GC回收KeyInput的。解决办法就是使用弱引用来替代全局引用:
JNIEXPORT jlong JNICALL
Java_KeyInput_create(JNIEnv *env, jobject self) {
key_input *cpp_obj = new key_input();
cpp_obj->back_ptr = env->NewWeakGlobalRef(self); return (jlong)cpp_obj; }
JNIEXPORT void JNICALL
Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer) {
key_input *cpp_obj = (key_input*)peer; env->DeleteWeakGlobalRef(cpp_obj->back_ptr); delete cpp_obj; return; }
第十章
本章总结了JNI实际应用中容易出错的一些情况供JNI程序员参考。
10.1 错误检查
编写本地方法时最常见的错误就是忘记检查是否发生了异常。我承认,JNI里面的异常检查确实比较麻烦,但是,这很重要。
10.2 向JNI函数传递非法参数
JNI不会检查参数是否正确,如果你自己不保证参数的正确有效,那么出现什么样的错误是未知的。通常,不检查参数的有效性在C/C++库中是比较常见的。
10.3 把jclass和jobject弄混
使用JNI时容易出错的地方
一开始使用JNI时,很容易把对象引用(jobject类型的值)和类引用(jclass类型的值)弄混。对象引用对应的是数组或者java.lang.Object及其子类的对象实例,而类引用对应的是java.lang.Class的实例。
像GetFieldID这样需要传入jclass作为参数的方法做的是一个类操作,因为它是从一个类中获取字段的描述。而GetIntField这样需要传入jobject作为参数的方法做的是一个对象操作,因为它从一个对象实例中获取字段的值。
10.4jboolean会面临数据截取的问题
Jboolean是一个8-bit unsigned的C类型,可以存储0~255的值。其中,0对应常量JNI_FALSE,而1~255对应常量JNI_TRUE。但是,32或者16位的值,如果最低的8位是0的话,就会引起问题。
假设你定义了一个函数print,需要传入一个jboolean类型的condition作为参数:
void print(jboolean condition) { /* C compilers generate code that truncates condition to its lower 8 bits. */ if (condition) {
printf(\ } else {
printf(\ } }
对上面这段代码来说,下面这样用就会出现问题:
int n = 256; /* the value 0x100, whose lower 8 bits are all 0 */ print(n);
我们传入了一个非0的值256(0X100),因为这个值的低8位(即,0)被截出来使用,上面的代码会打印“false”。
根据经验,这里有一个常用的解决方案:
n = 256;
print (n ? JNI_TRUE : JNI_FALSE);
10.5 编程的时候,什么用JAVA,什么时候用C?
这里有一些经验性的注意事项: 1、
尽量让JAVA和C之间的接口简单化,C和JAVA间的调用过于复杂的话,会使得BUG调试、代码维护和JVM对代码进行优化都会变得很难。比如虚拟机很容易对一些JAVA方法进行内联,但对本地方法却无能为力。 2、 3、
尽量少写本地代码。因为本地代码即不安全又是不可移植的,而且本地代码中的错误让本地代码尽量独立。也就是说,实际使用的时候,尽量让所有的本地方法都在同一检查很麻烦。
个包甚至同一个类中。
JNI把JVM的许多功能开发给了本地代码:类加载、对象创建、字段访问、方法调用、线程同步等。虽然用JAVA来做这些事情的时候很容易,但有时候,用本地代码来做很诱人。下面的代
码会告诉你,为什么用本地代码进行JAVA编程是愚蠢的。假设我们需要创建一个线程并启动它,JAVA代码这样写:
new JobThread().start();
而用本地代码却需要这样:
/* Assume these variables are precomputed and cached: * Class_JobThread: the class \ * MID_Thread_init: method ID of constructor * MID_Thread_start: method ID of Thread.start() */
aThreadObject =
(*env)->NewObject(env, Class_JobThread, MID_Thread_init); if (aThreadObject == NULL) { ... /* out of memory */ }
(*env)->CallVoidMethod(env, aThreadObject, MID_Thread_start); if ((*env)->ExceptionOccurred(env)) { ... /* thread did not start */ }
比较起来,本地代码写会使用编程变得复杂,代码量大,错误处理多。通常,如果不得不用本地代码来做这些事的话,在JAVA中提供一个辅助函数,并在本地代码中对这个辅助函数进行回调。
10.6 混淆ID和引用
本地代码中使用引用来访问JAVA对象,使用ID来访问方法和字段。
引用指向的是可以由本地代码来管理的JVM中的资源。比如DeleteLocalRef这个本地函数,允许本地代码删除一个局部引用。而字段和方法的ID由JVM来管理,只有它所属的类被unload时,才会失效。本地代码不能显式在删掉一个字段或者方法的ID。
本地代码可以创建多个引用并让它们指向相同的对象。比如,一个全局引用和一个局部引用可能指向相同的对象。而字段ID和方法ID是唯一的。比如类A定义了一个方法f,而类B从类A中继承了方法f,那么下面的调用结果是相同的:
jmethodID MID_A_f = (*env)->GetMethodID(env, A, \jmethodID MID_B_f = (*env)->GetMethodID(env, B, \
10.7 缓存字段ID和方法ID
这里有一个缓存ID的例子:
class C {
private int i; native void f(); }
下面是本地方法的实现,没有使用缓存ID。
// No field IDs cached. JNIEXPORT void JNICALL
Java_C_f(JNIEnv *env, jobject this) {
jclass cls = (*env)->GetObjectClass(env, this); ... /* error checking */
jfieldID fid = (*env)->GetFieldID(env, cls, \ ... /* error checking */
ival = (*env)->GetIntField(env, this, fid); ... /* ival now has the value of this.i */ }
上面的这些代码一般可以运行正确,但是下面的情况下,就出错了:
// Trouble in the absence of ID caching class D extends C { private int i; D() {
f(); // inherited from C } }
类D继承了类C,并且也有一个私有的字段i。
当在D的构造方法中调用f时,本地方法接收到的参数中,cls指向提类D的对象,fid指向的是D.i这个字段。在这个本地方法的末尾,ival里面是D.i的值,而不是C.i的值。这与你想象的是不一样的。
上面这种问题的解决方案是:
// Version that caches IDs in static initializers class C {
private int i; native void f();
private static native void initIDs(); static {
initIDs(); // Call an initializing native method } }
本地方法这样实现:
static jfieldID FID_C_i;
JNIEXPORT void JNICALL
Java_C_initIDs(JNIEnv *env, jclass cls) {
/* Get IDs to all fields/methods of C that native methods will need. */
FID_C_i = (*env)->GetFieldID(env, cls, \ }
JNIEXPORT void JNICALL
Java_C_f(JNIEnv *env, jobject this) {
ival = (*env)->GetIntField(env, this, FID_C_i); ... /* ival is always C.i, not D.i */ }