本章总结了JNI实际应用中容易出错的一些情况供JNI程序员参考。
10.1 错误检查
编写本地方法时最常见的错误就是忘记检查是否发生了异常。我承认,JNI里面的异常检查确实比较麻烦,但是,这很重要。
10.2 向JNI函数传递非法参数
JNI不会检查参数是否正确,如果你自己不保证参数的正确有效,那么出现什么样的错误是未知的。通常,不检查参数的有效性在C/C++库中是比较常见的。
10.3 把jclass和jobject弄混
一开始使用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("true\n");
} else {
printf("false\n");
}
}
对上面这段代码来说,下面这样用就会出现问题:
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 "JobThread"
* 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, "f", "()V");
jmethodID MID_B_f = (*env)->GetMethodID(env, B, "f", "()V");
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, "i", "I");
... /* 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, "i", "I");
}
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 */
}
字段ID在类C的静态初始时被计算并缓存下来,这样就可以确保缓存的是C.i的ID,因此,不管本地方法中接收到的jobject是哪个类的实例,访问的永远是C.i的值。
另外,同样的情况也可能会出现在方法ID上面。
10.8 Unicode字符串结尾
从GetStringChars和GetStringCritical两个方法获得的Unicode字符串不是以NULL结尾的,需要调用GetStringLength来获取字符串的长度。一些操作系统,如Windows NT中,Unicode字符串必须以两个’\0’结尾,这样的话,就不能直接把GetStringChars得到的字符串传递给Windows NT系统的API,而必须复制一份并在字符串的结尾加入两个“\0”
10.9 访问权限失效
在本地代码中,访问方法和变量时不受JAVA语言规定的限制。比如,可以修改private和final修饰的字段。并且,JNI中可以访问和修改heap中任意位置的内存。这些都会造成意想不到的结果。比如,本地代码中不应该修改java.lang.String和java.lang.Integer这样的不可变对象的内容。否则,会破坏JAVA规范。
10.10 忽视国际化
JVM中的字符串是Unicode字符序列,而本地字符串采用的是本地化的编码。实际编码的时候,我们经常需要使用像JNU_NewStringNative和JNU_GetStringNativeChars这样的工具函数来把Unicode编码的jstring转化成本地字符串,要对消息和文件名尤其关注,它们经常是需要国际化的,可能包含各种字符。
如果一个本地方法得到了一个文件名,必须把它转化成本地字符串之后才能传递给C库函数使用:
JNIEXPORT jint JNICALL
Java_MyFile_open(JNIEnv *env, jobject self, jstring name,
jint mode)
{
jint result;
char *cname = JNU_GetStringNativeChars(env, name);
if (cname == NULL) {
return 0;
}
result = open(cname, mode);
free(cname);
return result;
}
上例中,我们使用JNU_GetStringNativeChars把Unicode字符串转化成本地字符串。
10.11 确保释放VM资源
JNI编程时常见的错误之一就是忘记释放VM资源,尤其是在执行路径分支时,比如,有异常发生的时候:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
const jchar *cstr =
(*env)->GetStringChars(env, jstr, NULL);
if (cstr == NULL) {
return;
}
...
if (...) { /* exception occurred */
/* misses a ReleaseStringChars call */
return;
}
...
/* normal return */
(*env)->ReleaseStringChars(env, jstr, cstr);
}
忘记调用ReleaseStringChars可能导致jstring永远被VM给pin着不被回收。一个GetStringChars必然要对应着一个ReleaseStringChars,下面的代码就没有正确地释放VM资源:
/* The isCopy argument is misused here! */
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
jboolean isCopy;
const jchar *cstr = (*env)->GetStringChars(env, jstr,
&isCopy);
if (cstr == NULL) {
return;
}
... /* use cstr */
/* This is wrong. Always need to call ReleaseStringChars. */
if (isCopy) {
(*env)->ReleaseStringChars(env, jstr, cstr);
}
}
即使在isCopy的值是JNI_FALSE时,也应该调用ReleaseStringChars在unpin掉jstring。
10.12 过多的创建局部引用
大量的局部引用创建会浪费不必要的内存。一个局部引用会导致它本身和它所指向的对象都得不到回收。尤其要注意那些长时间运行的方法、创建局部引用的循环和工具函数,充分得利用Pus/PopLocalFrame来高效地管理局部引用。
10.13 使用已经失效的局部引用
局部引用只在一个本地方法的调用期间有效,方法执行完成后会被自动释放。本地代码不应该把存储局部引用存储到全局变量中在其它地方使用。
10.14 跨进程使用JNIEnv
JNIEnv这个指针只能在当前线程中使用,不要在其它线程中使用。
10.15 错误的线程模型(Thread Models)
搞不明白,不翻了。。。
本章是JNI设计思想的一个概述,在讲的过程中,如果有必要的话,还会对底层实现技术的原理做说明。本章也可以看作是JNIEnv指针、局部和全局引用、字段和方法ID等这些JNI主要技术的规范。有些地方我们可能还会提到一些技术是怎么样去实现的,但我们不会专注于具体的实现方式,主要还是讨论一些实现策略。
11.1 设计目标
JNI最重要的设计目标就是在不同操作系统上的JVM之间提供二进制兼容,做到一个本地库不需要重新编译就可以运行不同的系统的JVM上面。
为了达到这一点儿,JNI设计时不能关心JVM的内部实现,因为JVM的内部实现机制在不断地变,而我们必须保持JNI接口的稳定。
JNI的第二个设计目标就是高效。我们可能会看到,有时为了满足第一个目标,可能需要牺牲一点儿效率,因此,我们需要在平台无关和效率之间做一些选择。
最后,JNI必须是一个完整的体系。它必须提供足够多的JVM功能让本地程序完成一些有用的任务。
JNI不能只针对一款特定的JVM,而是要提供一系列标准的接口让程序员可以把他们的本地代码库加载到不同的JVM中去。有时,调用特定JVM下实现的接口可以提供效率,但更多的情况下,我们需要用更通用的接口来解决问题。
11.2 加载本地库
在JAVA程序可以调用一个本地方法之间,JVM必须先加载一个包含这个本地方法的本地库。
11.2.1 类加载器
本地库通过类加载器定位。类加载器在JVM中有很多用途,如,加载类文件、定义类和接口、提供命令空间机制、定位本地库等。在这里,我们会假设你对类加载器的基本原理已经了解,我们会直接讲述加载器加载和链接类的技术细节。每一个类或者接口都会与最初读取它的class文件并创建类或接口对象的那个类加载器关联起来。只有在名字和定义它们的类加载器都相同的情况下,两个类或者接口的类型才会一致。例如,图11.1中,类加载器L1和L2都定义了一个名字为C的类。这两个类并不相同,因为它们包含了两个不同的f方法,因为它们的f方法返回类型不同。
图11.1 两个名字相同的类被不同类加载器加载的情况
上图中的点划线表达了类加载器之间的关系。一个类加载器必须请求其它类加载器为它加载类或者接口。例如,L1和L2都委托系统类加载器来加载系统类java.lang.String。委托机制,允许不同的类加载器分离系统类。因为L1和L2都委托了系统类加载器来加载系统类,所以被系统类加载器加载的系统类可以在L1和L2之间共享。这种思想很必要,因为如果程序或者系统代码对java.lang.String有不同的理解的话,就会出现类型安全问题。
11.2.2 类加载器和本地库
如图11.2,假设两个C类都有一个方法f。VM使用“C_f”来定位两个C.f方法的本地代码实现。为了确保类C被链接到了正确的本地函数,每一个类加载器都会保存一个与自己相关联的本地库列表。
图11.2 类加载器和本地库的关联
正是由于每一个类加载器都保存着一个本地库列表,所以,只要是被这个类加载器加载的类,都可以使用这个本地库中的本地方法。因此,程序员可以使用一个单一的库来存储所有的本地方法。
当类加载器被回收时,本地库也会被JVM自动被unload。
11.2.3 定位本地库
本地库通过System.loadLibrary方法来加载。下面的例子中,类Cls静态初始化时加载了一个本地库,f方法就是定义在这个库中的。
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary("mypkg");
}
}
JVM会根据当前系统环境的不同,把库的名字转换成相应的本地库名字。例如,Solaris下,mypkg会被转化成libmypkg.so,而Win32环境下,被转化成mypkg.dll。
JVM在启动的时候,会生成一个本地库的目录列表,这个列表的具体内容依赖于当前的系统环境,比如Win32下,这个列表中会包含Windows系统目录、当前工作目录、PATH环境变量里面的目录。
System.loadLibrary在加载相应本地库失败时,会抛出UnsatisfiedLinkError错误。如果相应的库已经加载过,这个方法不做任何事情。如果底层操作系统不支持动态链接,那么所有的本地方法必须被prelink到VM上,这样的话,VM中调用System.loadLibrary时实际上没有加载任何库。
JVM内部为每一个类加载器都维护了一个已经加载的本地库的列表。它通过三步来决定一个新加载的本地库应该和哪个类加载器关联。
1、 确定System.loadLibrary的调用者。
2、 确定定义调用者的类。
3、 确定类的加载器。
下面的例子中,JVM会把本地库foo和定义C的类加载器关联起来。
class C {
static {
System.loadLibrary("foo");
}
}
11.2.4 类型安全保障措施
VM中规定,一个JNI本地库只能被一个类加载器加载。当一个JNI本地库已经被第一个类加载器加载后,第二个类加载器再加载时,会报UnsatisfiedLinkError。这样规定的目的是为了确保基于类加载器的命令空间分隔机制在本地库中同样有效。如果不这样的话,通过本地方法进行操作JVM时,很容易造成属于不同类加载器的类和接口的混乱。下面代码中,本地方法Foo.f中缓存了一个全局引用,指向类Foo:
JNIEXPORT void JNICALL
Java_Foo_f(JNIEnv *env, jobject self)
{
static jclass cachedFooClass; /* cached class Foo */
if (cachedFooClass == NULL) {
jclass fooClass = (*env)->FindClass(env, "Foo");
if (fooClass == NULL) {
return; /* error */
}
cachedFooClass = (*env)->NewGlobalRef(env, fooClass);
if (cachedFooClass == NULL) {
return; /* error */
}
}
assert((*env)->IsInstanceOf(env, self, cachedFooClass));
... /* use cachedFooClass */
}
上面的例子中,因为Foo.f是一个实例方法,而self指向一个Foo的实例对象,所以,我们认为最后那个assertion会执行成功。但是,如果L1和L2分别加载了两个不同的Foo类,而这两个Foo类都被链接到Foo.f的实现上的话,assertion可能会执行失败。因为,哪个Foo类的f方法首先被调用,全局引用cachedFooClass指向的就是哪个Foo类。
11.2.5 unload本地库
一旦JVM回收类加载器,与这个类加载器关联的本地库就会被unload。因为类指向它自己的加载器,所以,这意味着,VM也会被这个类unload。
11.3 链接本地方法
VM会在第一次使用一个本地方法的时候链接它。假设调用了方法g,而在g的方法体中出现了对方法f的调用,那么本地方法f就会被链接。VM不应该过早地链接本地方法,因为这时候实现这些本地方法的本地库可能还没有被load,从而导致链接错误。
链接一个本地方法需要下面这几个步骤:
1、 确定定义了本地方法的类的加载器。
2、 在加载器所关联的本地库列表中搜索实现了本地方法的本地函数。
3、 建立内部的数据结构,使对本地方法的调用可能直接定向到本地函数。
VM通过下面这几步,同本地方法的名字生成与之对应的本地函数的名字:
1、 前缀“Java_”。
2、 类的全名。
3、 下划线分隔符“_”。
4、 方法名字。
5、 有方法重载的情况时,还会有两个下划线(“__”),后面跟着参数描述符。
VM在类加载器关联的本地库中搜索符合指定名字的本地函数。对每一个库进行搜索时,VM会先搜索短名字(short name),即没有参数描述符的名字。然后搜索长名字(long name),即有参数描述符的名字。当两个本地方法重载时,程序员需要使用长名字来搜索。但如果一个本地方法和一个非本地方法重载时,就不会使用长名字。
JNI使用一种简单的名字编码协议来确保所有的Unicode字符都被转化成可用的C函数名字。用下划线(“_”)分隔类的全名中的各部分,取代原来的点(“.”)。
如果多个本地库中都存在与一个编码后的本地方法名字相匹配的本地函数,哪个本地库首先被加载,则它里面的本地函数就与这个本地方法链接。如果没有哪个函数与给定的本地方法相匹配,则UnsatisfiedLinkError被抛出。
程序员还可以调用JNI函数RegisterNatives来注册与一个类关联的本地方法。这个JNI函数对静态链接函数非常有用。
11.4 调用转换(calling convention)
调用转换决定了一个本地函数如何接收参数和返回结果。目前没有一个标准,主要取决于编译器和本地语言的不同。JNI要求同一个系统环境下,调用转换机制必须相同。例如,JNI在UNIX下使用C调用转换,而在Win32下使用stdcall调用转换。
如果程序员需要调用的函数遵循不同的调用转换机制,那么最好写一个转换层来解决这个问题。
11.5 JNIEnv接口指针
JNIEnv是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在这个表中,每一个函数位于一个预定义的位置上面。JNIEnv很像一个C++虚函数表或者Microsoft COM接口。图11.3演示了这种关系。
图11.3 线程的局部JNIEnv接口指针
如果一个函数实现了一个本地方法,那么这个函数的第一个参数就是一个JNIEnv接口指针。从同一个线程中调用的本地方法,传入的JNIEnv指针是相同的。本地方法可能被不同的线程调用,这时,传入的JNIEnv指针是不同的。但JNIEnv间接指向的函数表在多个线程间是共享的。
JNI指针指向一个线程内的局部数据结构是因为一些平台上面没有对线程局部存储访问的有效支持。
因为JNIEnv指针是线程局部的,本地代码决不能跨线程使用JNIEnv。
11.5.2 接口指针的好处
比起写死一个函数入口来说,使用接口指针可以有以下几个优点:
1、 JNI函数表是作为参数传递给每一个本地方法的,这样的话,本地库就不必与特定的JVM关联起来。这使得JNI可以在不同的JVM间通用。
2、 JVM可以提供几个不同的函数表,用于不同的场合。比如,JVM可以提供两个版本的JNI函数表,一个做较多的错误检查,用于调试时;另外一个做较少的错误检查,更高效,用于发布时。
11.6 传递数据
像int、char等这样的基本数据类型,在本地代码和JVM之间进行复制传递,而对象是引用传递的。每一个引用都包含一个指向JVM中相应的对象的指针,但本地代码不能直接使用这个指针,必须通过引用来间接使用。
比起传递直接指针来说,传递引用可以让VM更灵活地管理对象。比如,你在本地代码中抓着一个引用的时候,VM那小子可能这个时候正偷偷摸摸地把这个引用间接指向的那个对象从一块儿内存区域给挪到另一块儿。不过,有一点儿你放心,VM是不敢动对象里面的内容的,因为引用的有效性它要负责。瞅一下图11.4,你就会得道了。
图11.4 本地代码抓着引用时,VM的偷鸡摸狗
11.6.1 全局引用和局部引用这对好哥儿们
本地代码中,可以通过JNI创建两种引用,全局引用和局部引用。局部引用的有效期是本地方法的调用期间,调用完成后,局部引用会被JVM自动铲除。而全局引用呢,只要你不手动把它干掉,它会一直站在那里。
JVM中的对象作为参数传递给本地方法时,用的是局部引用。大部分的JNI函数返回局部引用。JNI允许程序员从局部引用创建一个全局引用。接受对象作为参数的JNI函数既支持全局引用也支持局部引用。本地方法执行完毕后,向JVM返回结果时,它可能向JVM返回局部引用,也可能返回全局引用。
局部引用只在创建它的线程内部有效。本地代码不能跨线程传递和使用局部引用。
JNI中的NULL引用指向JVM中的null对象。对一个全局引用或者局部引用来说,只要它的值不是NULL,它就不会指向一个null对象。
11.6.2 局部引用的内部实现
一个对象从JVM传递给本地方法时,就把控制权移交了过去,JVM会为每一个对象的传递创建一条记录,一条记录就是一个本地代码中的引用和JVM中的对象的一个映射。记录中的对象不会被GC回收。所有传递到本地代码中的对象和从JNI函数返回的对象都被自动地添加到映射表中。当本地方法返回时,VM会删除这些映射,允许GC回收记录中的数据。图11.5演示了局部引用记录是怎么样被创建和删除的。一个JVM窗口对应一个本地方法,窗口里面包含了一个指向局部引用映射表的指针。方法D.f调用本地方法C.g。C.g通过C函数Java_C_g来实现。在进入到Java_C_g之前,虚拟机会创建一个局部引用映射表,当Java_C_g返回时,VM会删掉这个局部引用映射表。
图11.5 创建和删除局部引用映射表
有许多方式可以实现一个映射表,比如栈、表、链表、哈希表。实现时可能会使用引用计数来避免重得。
11.6.3 弱引用
弱引用所指向的对象允许JVM回收,当对象被回收以后,弱引用也会被清除。
11.7 对象访问
JNI提供丰富的函数让本地代码通过引用来操作对象,而不用操心JVM内部如何实现。使用JNI函数来通过引用间接操作对象比使用指针直接操作C中的对象要慢。但是,我们认为这很值得。
11.7.1 访问基本类型数组
访问数组时,如果用JNI函数重复调用访问其中的每一个元素,那么消耗是相当大的。
一个解决方案是引入一种“pin”机制,这样JVM就不会再移动数组内容。本地方法接受一个指向这些元素的直接指针。但这有两个影响:
1、 JVM的GC必须支持“pin”。“pin”机制在JVM中并不是一定要实现的,因为它会使GC的算法更复杂,并有可能导致内存碎片。
2、 JVM必须在内存中连续地存放数组。虽然这是大部分基本类型数组的默认实现方式,但是boolean数组是比较特殊的一个。Boolean数组有两种方式,packed和unpacked。用packed实现方式时,每个元素用一个bit来存放一个元素,而unpacked使用一个字节来存放一个元素。因此,依赖于boolean数组特定存放方式的本地代码将是不可移植的。
JNI采用了一个折衷方案来解决上面这两个问题。
首先,JNI提供了一系列函数(例如,GetIntArrayRegion、SetIntArrayRegion)把基本类型数组复制到本地的内存缓存。如果本地代码需要访问数组当中的少量元素,或者必须要复制一份的话,请使用这些函数。
其次,程序可以使用另外一组函数(例如,GetIntArrayElement)来获取数组被pin后的直接指针。如果VM不支持pin,这组函数会返回数组的复本。这组函数是否会复制数组,取决于下面两点:
1、 如果GC支持pin,并且数组的布局和本地相同类型的数组布局一样,就不会发生复制。
2、 否则的话,数组被复制到一个不可变的内存块儿中(例如,C的heap上面)并做一些格式转换。并把复制品的指针返回。
当数组使用完后,本地代码会调用另外一组函数(例如,ReleaseInt-ArrayElement)来通知JVM。这时,JVM会unpin数组或者把对复制后的数组的改变反映到原数组上然后释放复制后的数组。
这种方式提供了很大的灵活性。GC算法可以自由决定是复制数组,或者pin数组,还是复制小数组,pin大数组。
JNI函数必须确保不同线程的本地方法可以同步访问相同的数组。例如,JNI可能会为每一个被pin的数组保持一个计数器,如果数组被两个线程pin的话,其中一个unpin不会影响另一个线程。
11.7.2 字段和方法
JNI允许本地代码通过名字和类型描述符来访问JAVA中的字段或调用JAVA中的方法。例如,为了读取类cls中的一个int实例字段,本地方法首先要获取字段ID:
jfieldID fid = env->GetFieldID(env, cls, "i", "I");
然后可以多次使用这个ID,不需要再次查找:
jint value = env->GetIntField(env, obj, fid);
除非JVM把定义这个字段和方法的类或者接口unload,字段ID和方法ID会一直有效。
字段和方法可以来自定个类或接口,也可以来自它们的父类或间接父类。JVM规范规定:如果两个类或者接口定义了相同的字段和方法,那么它们返回的字段ID和方法ID也一定会相同。例如,如果类B定义了字段fld,类C从B继承了字段fld,那么程序从这两个类上获取到的名字为“fld”的字段的字段ID是相同的。
JNI不会规定字段ID和方法ID在JVM内部如何实现。
通过JNI,程序只能访问那些已经知道名字和类型的字段和方法。而使用Java Core Reflection机制提供的API,程序员不用知道具体的信息就可以访问字段或者调用方法。有时在本地代码中调用反射机制也很有用。所以,JDK提供了一组API来在JNI字段ID和java.lang.reflect.Field类的实例之间转换,另外一组在JNI方法ID和java.lang.reflect.Method类实例之间转换。
11.8 错误和异常
JNI编程时的错误通常是JNI函数的误用导致的。比如,向GetFieldID方法传递一个对象引用而不是类引用等。
11.8.1 不检查编程错误
JNI函数不对编程错误进行检查。向JNI函数传递非法参数会导致未知的行为。原因如下:
1、 强制JNI函数检查所有可能的错误会减慢所有本地方法的执行效率。
2、 大部分情况下,运行时没有足够的类型信息来做错误检查。
大部分的C库函数也同样对编程错误不做预防。例如printf这个函数,当接收到非法的参数时,它会引发一起运行时错误,而不会抛出错误码。强制C库函数检查所有可能的错误会导致错误被重复检查,一次是在用户代码中,一次是在库函数中。
虽然JNI规范没有要求VM检查编程错误,但鼓励VM对普通错误提供检查功能。例如,VM在使用JNI函数表的调用版本时可能会做更多的错误检查。
11.8.2 JVM异常
一旦JNI发生错误,必须依赖于JVM来处理异常。通过调用Throw或者ThrowNew来向JVM抛出一个异常。一个未被处理的异常会记录在当前线程中。和JAVA中的异常不同,本地代码中的异常不会立即中断当前的程序执行。
本地代码中没有标准的异常处理机制,因此,JNI程序最好在每一步可能会产生异常的操作后面都检查和处理异常。JNI程序员处理异常通常有两种方式:
1、 本地方法可以选择立即返回。让代码中抛出的异常向调用者抛出。
2、 本地代码可以通过调用ExceptionClear清理异常并运行自己的异常处理代码。
异常发生后,一定要先进行处理或者清除后再进行后续的JNI函数调用。大部分情况下,调用一个未被处理的异常都可能会一个未定义的结果。下面列表中的JNI函数可以在发生异常后安全地调用:
· ExceptionOccurred
· ExceptionDescribe
· ExceptionClear
· ExceptionCheck
·
· ReleaseStringChars
· ReleaseStringUTFchars
· ReleaseStringCritical
· Release<Type>ArrayElements
· ReleasePrimitiveArrayCritical
· DeleteLocalRef
· DeleteGlobalRef
· DeleteWeakGlobalRef
· MonitorExit
最前面的四个函数都是用来做异常处理的。剩下的都是用来释放资源的,通常,异常发生后都需要释放资源。
11.8.3 异步异常
本节已经过时,不再翻译。