第三章 基本类型、字符串和数组
当面对Java应用程序混合本地编程语言代码时,程序员经常会问的一个问题是:Java编程语言中的数据类型是如何映射到C/C++等本地编程语言中的数据类型的。上一章中介绍的“Hello World!”示例中,我们没有任何的参数需要传输给本地方法,本地方法也没有返回任何结果给调用者。本地方法只是简单的打印一条信息然后返回。
在实践中,许多程序都需要传送参数给本地方法,而且需要从本地方法中获取返回值。本章,我们将介绍如何在Java编程语言编写的代码和本地编程语言编写的代码之间交换数据类型。我们从原始类型,例如整型和常用的对象类型,例如字符串和数组开始讲解。我们将把任意对象的完整处理留到下一章,下一章我们将会介绍本地方法的代码如何访问字段和进行方法调用。
3.1 一个简单的本地方法
让我们先从一个简单的程序开始,这个程序和上一章的HelloWorld程序没有多少不同。示例程序,Prompt.java,包含一个打印一个字符串、等待用户输入、最后将用户输入的内容返回给调用函数的本地方法。该程序的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Prompt { // native method that prints a prompt and reads a line private native String getLine(String prompt); public static void main(String args[]) { Prompt p = new Prompt(); String input = p.getLine("Type a line: "); System.out.println("User typed: " + input); } static { System.loadLibrary("Prompt"); } } |
Prompt.main调用本地方法Prompt.getLine来获取用户的输入。在静态初始化块中调用System.loadLibrary方法将名为Prompt的本地库加载到程序中。
3.1.1 本地方法的C原型
Prompt.getLine方法可以使用以下C函数实现:
1 2 |
JNIEXPORT jstring JNICALL Java_Prompt_getLine (JNIEnv *, jobject, jstring); |
你可以使用javah工具来生成包含上述函数原型的头文件。JNIEXPORT和JNICALL宏(在JNI.h都在文件中定义)确保这个函数从本地库中导出而且C编译器使用该函数的正确调用约定生成代码。C函数的名称是通过连接“Java_”前缀、类名称和方法名称构成的。11.3节包含如何形成C函数名称的更准确的描述。
3.1.2 本地方法参数
如2.4节所述,本地方法实现(如Java_Prompt_getLine)除了在本地方法中声明的参数外,还接受两个标准参数。第一个参数是JNIEnv接口指针,指向函数表指针的位置。每个方法表中的指针指向一个JNI函数。本地方法始终通过JNI函数之一访问Java虚拟机中的数据结构。如图3.1所示JNIEnv接口指针:
第二个参数取决于本地方法是实例方法静态方法还是实例方法。实例化本地方法的第二个参数是对该方法的调用对象的应用,与C++语言中的this指针类似。静态本地方法的第二个参数是对定义该方法的类的应用。我们的例子,Java_Prompt_getLine实现为一个实例化本地方法。所以第二个参数jobject就是对对象本身的引用。
3.1.3 类型映射
本地方法声明中的参数类型在本地编程语言中都有相应的类型。JNI中定义了一组对应于Java编程语言中类型的C/C++类型。在Java编程语言中存在着两种类型:基本类型,如int、float和char以及引用类型,如类、实例和数组。在Java编程语言中字符串是java.lang.String类的实例化。
JNI以不同的方式处理基本类型和引用类型。基本类型的映射是直接的。Java编程语言中的int类型映射为C/C++的jint(定义在jni.h中,为32位有符号整型数),Java编程语言中的float类型映射为C/C++的jfloat(定义在jni.h中,为32位浮点类型数)。12.1.1节包含所有在JNI中定义的基本类型(这里简单把截图放一下,如下图)。
JNI传递对象给本地方法作为不透明引用。不透明引用指的是引用Java虚拟机内部数据类型的C指针类型。对于程序员而言,Java虚拟机内部数据的准确布局是隐藏的。本地代码可以通过JNIEnv接口指针指向的适当的函数来操作底层对象。例如,java.lang.String对应于JNI类型jstring,jstring引用的确切位置和本地代码是不相关的。本地代码通过jni函数,例如GetStringUTFChars来获取string的内容。
所有的JNI类型都有类型jobject(感觉应该是jobject的子类的意思),为了方便和增强类型安全性,JNI定义了一组在概念上是jobject的子类型的引用类型(A是B的子类型,那么A的每个实例对象都会是B的实例对象(博主注:大概应该是这个意思吧))。这些子类型对应于Java编程语言中经常使用的引用类型。例如:jstring表示字符串,jobjectArray表示对象数据,12.1.2节(这里也简单把截图放一下,如下图)完整的列出了JNI参考类型及其子类型的关系。
3.2 访问字符串
Java_Prompt_getLine接收prompt参数为一个jstring类型。jstring类型在Java虚拟机代表着字符串,但是有不同于常规的C字符串(指向字符的指针,char *)。你不能将jstring作为常规C字符串来使用。运行下面的代码将不会得到期望的结果,而事实上很可能会导致Java虚拟机崩溃。
1 2 3 4 5 6 |
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { /* ERROR: incorrect use of jstring as a char* pointer */ printf("%s", prompt); ... } |
3.2.1 转换为本地字符串
你的本地代码必须使用恰当的JNI函数将jstring对象转换为C/C++字符串。JNI同时支持将jstring转换为Unicode和UTF-8字符串。Unicode字符串使用16位值表示字符,而UTF-8字符串则使用向上兼容7位ASCII字符串的编码方法。尽管UTF-8字符串包含非ASCII字符,但是其表现类似于使用NULL终止符的C字符串。值在1到127之间的所有7位ASCII字符在UTF-8字符串中编码保持不变。一个字节中的最高位被设置了,表示多字节编码的16位Unicode值的开始。
Java_Prompt_getLine方法调用JNI方法GetStringUTFChars来读取字符串的内容。可以通过JNIEnv接口指针使用GetStringUTFChars方法。它将通常在Java虚拟机中实现为Unicode序列的jstring引用转换为UTF-8格式的C式字符串。如果你可以确定原始的字符串只包含7位ASCII字符,你可以将转换后的字符串传给C库函数,例如printf(我们会在8.2节讨论佮处理非ASCII字符串)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include "Prompt.h" #include <stdio.h> JNIEXPORT jstring JNICALL Java_Prompt_getLine (JNIEnv *env, jobject obj, jstring prompt) { char buf[128]; const jbyte *str; str = (*env)->GetStringUTFChars(env, prompt, NULL); if(str == NULL) return NULL; printf("%s", str); (*env)->ReleaseStringUTFChars(env, prompt, str); /* We assume here that the user does not type more than * 127 characters */ scanf("%s", buf); return (*env)->NewStringUTF(env, buf); } |
不要忘记检查GetStringUTFChars的返回值,这是因为Java虚拟机的实现决定内部需要申请内存来容纳UTF-8字符串,内存的申请是有可能会失败的。如果内存申请失败,那么GetStringUTFChars将会返回NULL并且会抛出OutOfMemoryError异常。正如我们会在第六章介绍的一样,在JNI中抛出异常和在Java中抛出异常是不同的。通过JNI抛出的挂起异常不会自动更改本地C代码的控制流程。相反我们需要一个显示的return语句来跳过C函数中的剩余语句。Java_Prompt_getLine返回后,该异常会返回给Prompt.getLine的调用者Prompt.main函数中。
3.2.2 释放本地字符串资源
当你的本地代码使用完通过GetStringUTFChars获取的UTF-8字符串后,需要调用ReleaseStringUTFChars,调用ReleaseStringUTFChars表明本地代码不再需要GetStringUTFChars返回的UTF-8字符串了,调用ReleaseStringUTFChars就能够释放掉UTF-8字符串占用的内存。不调用ReleaseStringUTFChars进行内存释放会导致内存泄漏,最终导致内存耗尽。
3.2.3 创建新字符串
通过调用JNI函数NewStringUTF,你可以在本地代码中创建一个新的java.lang.String实例。NewStringUTF方法使用一个UTF-8格式的C式字符串作为参数并生成一个java.lang.String实例对象。新构造的java.lang.String实例和给定的UTF-8 C式字符串有相同的Unicode序列。如果虚拟机没办法申请足够的内存来构造java.lang.String实例的话,NewStringUTF会抛出一个OutOfMemoryError异常并返回NULL。在这个例子中,我们不需要检查返回值,因为本地代码会在之后立刻返回。如果NewStringUTF调用失败,OutOfMemoryError异常会在该方法的调用者Prompt.main中被抛出。如果NewStringUTF调用成功,它会返回一个指向新构造的java.lang.String对象的引用。这个新构造的实例会在Prompt.getLine中返回,并在Prompt.main中赋值给input。
3.2.4 其他JNI字符串方法
除了之前介绍的GetStringUTFChars、ReleaseStringUTFChars和NewStringUTF函数外,JNI中还支持其他的字符串相关方法。GetStringChars和ReleaseStringChars获取以Unicode格式表示的字符串字符。当操作系统支持将Unicode作为本地字符串格式的时候,这些函数将会非常有用。
UTF-8字符串常以‘\0’结尾,而Unicode字符串却不是。为了统计一个jstring引用中的Unicode字符个数时,JNI程序员可以调用GetStringLength。为了统计一个UTF-8格式的jstring对象占用多少字节时,可以对GetStringUTFChars的返回值调用ANSI C函数strlen来获得,或者直接对jstring引用调用JNI函数GetStringUTFLength来获得。GetStringUTFChars和GetStringChars方法的第三个参数需要做些而外的解释:
const jchar *GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
从GetStringChars方法返时,如果返回的字符串是原始java.lang.String实例中的字符的副本,那么isCopy指向的内存地址的值被设置为JNI_TURE。如果返回的字符串是原始java.lang.String实例中字符的的直接引用,那么isCopy指向的内存地址的值被设置为JNI_FALSE。当isCopy指向的内存地址的值被设置为JNI_FALSE时,本地代码不能修改返回的字符串的内容。如果违反该规则,将会导致原始的java.lang.String实例对象也被修改。这将打破java.lang.String不可修改规则。
大部分情况是将NULL作为isCopy的参数传递给方法,因为你不用关注Java虚拟机返回的是java.lang.String实例的副本还是直接引用。
通常无法预测虚拟机是否会复制给定的java.lang.String中的字符。因此程序员必须了解到诸如GetStringChars之类的函数可能需要与java.lang.String实例中的字符数成比例的时间和空间开销。在典型的Java虚拟机实现中,垃圾收集器重新定位堆中的对象。一旦将指向java.lang.String实例的直接指针传回给本地代码中,垃圾收集器将不能重新定位java.lang.String实例。换句话说,虚拟机必须固定java.lang.String实例,因为过多的固定会导致内存碎片,所以虚拟机实现可以自由的选择为每个GetStringChars调用复制字符还是固定实例。
当你不再需要访问GetStringChars函数返回的字符串元素时,不要忘记调用ReleaseStringChars。不管GetStringChars中的isCopy设置为JNI_FALSE还是JNI_TRUE,ReleaseStringChars都是必须调用的。ReleaseStringChars会释放副本或者取消固定实例,具体取决于GetStringChars返回实例的副本还是固定实例。
3.2.5 在Java 2 SDK 1.2中新添加的JNI字符串函数
为了增加虚拟机返回java.lang.String实例字符的直接指针的可能性,Java 2 SDK版本1.2引入了一组新的Get/ReleaseStringCritical函数。 在表面上,它们似乎与Get/ReleaseStringChars函数类似,如果可能的话,它们返回一个指向字符的指针; 否则,会复制一份。 但是,如何使用这些功能有很大的限制。
你必须将这对函数里的代码视为在临界区中运行,在临界区内,本地代码不能随意(arbitrary,博主这里翻译为随意的,但是感觉有点不对,但是不知道怎么翻译好,独占?感觉跟Linux内核中断处理有点像)调用JNI函数或者其他任意的会引起当前线程阻塞以及会等待Java虚拟机中另一个线程的本地函数。例如,当前线程不能够等待另一个线程的I/O输入流。这些限制使得虚拟机可以在本地代码持有通过GetStringCritical获取的字符串元素的直接指针时禁用垃圾回收。当垃圾收集器被禁用,其他触发垃圾收集器的线程都会被挂起。在Get/ReleaseStringCritical对之间的本地代码不能调用回引起阻塞的调用以及创建新对象。否则虚拟机可能会引起死锁,考虑如下场景:
- 由另一个线程触发的垃圾回收无法进行,直到当前线程完成阻塞调用并重新启用垃圾回收。
- 同时,当前的线程无法进行,因为阻塞调用需要获得一个已经被另一个正在等待执行垃圾回收的线程持有的锁。
重叠调用多对GetStringCritical和ReleaseStringCritical是安全的。例如:
1 2 3 4 5 6 7 |
jchar *s1, *s2; s1 = (*env)->GetStringCritical(env, jstr1); if (s1 == NULL) { ... /* error handling */ } s2 = (*env)->GetStringCritical(env, jstr2); if (s2 == NULL) { (*env)->ReleaseStringCritical(env, jstr1, s1); ... /* error handling */ } ... /* use s1 and s2 */ (*env)->ReleaseStringCritical(env, jstr1, s1); (*env)->ReleaseStringCritical(env, jstr2, s2); |
Get/ReleaseStringCritical对的使用不需要以堆栈顺序严格嵌套。我们不能忘记检查其因内存不足导致返回值为NULL的情况,因为GetStringCritical仍然可以分配一个缓冲区,并且如果虚拟机内部表示不同格式的数组,则复制数组。例如,Java虚拟机可能不会连续存储数组。在这种情况下,GetStringCritical必须复制jstring实例中的所有字符,以便将本地代码的连续字符数组返回。
为了避免死锁,你应该确保你的本地代码在调用GetStringCritical之后和在调用ReleaseStringCritical之前不应当随意调用JNI函数,在临界区中唯一允许的JNI函数是嵌套调用Get/ReleaseStringCritical和Get/ReleasePrimitiveArrayCritical。
JNI不支持GetStringUTFCritical和ReleaseStringUTFCritical方法。这些函数几乎都需要虚拟机创建字符串的副本,这是因为虚拟机实现几乎在内部都是使用Unicode格式来存储字符串。
另外在Java SDK 2 Release 1.2中增加的函数是GetStringRegion和GetStringUTFRegion。这些函数将字符串元素复制到一个预先分配的内存当中。Prompt.getLine方法可以使用GetStringUTFRegion进行重写:
1 2 3 4 5 6 7 8 9 10 |
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { /* assume the prompt string and user input has less than 128 characters */ char outbuf[128], inbuf[128]; int len = (*env)->GetStringLength(env, prompt); (*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf); printf("%s", outbuf); scanf("%s", inbuf); return (*env)->NewStringUTF(env, inbuf); } |
GetStringUTFRegion将字符串开始的下标和长度作为参数,这两个值都是以Unicode字符来统计。这个函数同时做边界检查,同时有必要会抛出StringIndexOutOfBoundsExecption异常。上面的代码中,我们从字符串引用本身获取到长度,因此可以确认不会出现下标越界(但是上面的代码缺少对prompt的检查,以确保其长度低于128个字符)。
3.2.6 JNI字符串函数总结
&esmp;表3.1中列出所有字符串相关的JNI函数,Java 2 SDK 1.2版本增加了一些增强某些字符串操作性能的新功能。 除了提高性能之外,增加的功能不支持新的操作。
表3.1 JNI字符串函数总结
JNI函数 | 描述 | 从哪个版本开始 |
---|---|---|
GetStringChars\ReleaseStringChars | 获取或释放指向Unicode格式的字符串内容的指针。可能会返回字符串的副本。 | JDK 1.1 |
GetStringUTFChars\ReleaseStringUTFChars | 获取或释放指向UTF-8格式的字符串内容指针。可能会返回字符串的副本 | JDK 1.1 |
GetStringLength | 返回字符串中Unicode字符的数量 | JDK 1.1 |
GetStringUTFLength | 返回以UTF-8格式表示字符串所需的字节数(不包括尾数0)。 | JDK 1.1 |
NewString | 创建拥有和给定的Unicode格式C式字符串相同字符序列的java.lang.String实例 | JDK 1.1 |
NewStringUTF | 创建拥有和给定的UTF-8格式C式字符串相同字符序列的java.lang.String实例 | JDK 1.1 |
GetStringCritical\ReleaseStringCritical | 获取指向Unicode格式的字符串内容的指针。 可能会返回字符串的副本。本地代码不能在Get/ReleaseStringCritical调用中间阻塞 | Java 2 SDK 1.2 |
GetStringRegion\SetStringRegion | 以Unicode格式将字符串的内容复制到预分配的C缓冲器到或从预分配的C缓冲区中复制。 | Java 2 JDK 1.2 |
GetStringUTFRegion\SetStringUTFRegion | 以UTF-8格式将字符串的内容复制到预分配的C缓冲区中或从预分配的C缓冲区中复制 | Java 2 JDK 1.2 |
3.2.7 选择合适的字符串函数
图3.2中表明程序员应该如何在JDK release 1.1和Java 2 SDK release 1.2中选择合适字符串相关函数。
如果你使用的是1.1或者1.1和1.2发行版的JDK,那么除了Get/ReleaseStringChars和Get/ReleaseStringUTFChars外没有其余的选择了。
如果你是使用Java 2 JDK release 1.2及以上的版本进行编程,并且想字符串的内容复制到已经分配了C缓冲区中,使用GetStringRegion或者GetStringUTFRegion。
对于小型固定大小的字符串,Get/SetStringRegion和Get/SetStringUTFRegion几乎总是首选函数,因为C缓冲区在C堆栈上进行分配是开销是非常小的。在字符串中复制少量字符的开销是微不足道的。
Get/SetStringRegion和Get/SetStringUTFRegion的一个优点是它们不执行内存分配,因此不会引起意外的内存不足异常。 如果确保不能发生索引溢出,则不需要进行异常检查。Get/SetStringRegion和Get/SetStringUTFRegion的另一个优点是您可以指定起始索引和字符数。 如果本地代码仅需要访问长字符串中的字符子集,那么这些函数是合适的。
使用GetStringCritical函数必须非常小心。你必须确保当持有一个通过GetStringCritical返回的指针时,本地代码在Java虚拟机中不会创建新对象或者会引起系统死锁的阻塞性调用。
下面是一个实例,演示了使用GetStringCritical产生的微妙问题。下面的代码获取字符串的内容,并调用fprintf函数将字符写入到文件句柄fd中:
1 2 3 4 5 6 7 |
/* This is not safe! */ const char *c_str = (*env)->GetStringCritical(env, j_str, 0); if (c_str == NULL) { .../* error handling */ } fprintf(fd, "%s\n", c_str); (*env)->ReleaseStringCritical(env, j_str, c_str); |
上述代码的问题是当当前线程禁用垃圾收集时,写入文件句柄并不总是安全的。假设,例如,另一个线程T等待从fd文件句柄读取。 让我们进一步假设操作系统缓冲的设置方式使得fprintf调用等待,直到线程T完成从fd读取所有挂起的数据。我们已经构建了可能的死锁场景:如果线程T不能分配足够的内存 作为从文件句柄读取的缓冲区,它必须请求垃圾回收。 垃圾回收请求将被阻止,直到当前线程执行ReleaseStringCritical,直到fprintf调用返回为止。 然而,fprintf调用正在等待线程T从文件句柄中完成读取。
以下代码虽然与上述示例类似,但几乎肯定是无死锁的:
1 2 3 4 5 6 7 |
/* This code segment is OK. */ const char *c_str = (*env)->GetStringCritical(env, j_str, 0); if (c_str == NULL) { ... /* error handling */ } DrawString(c_str); (*env)->ReleaseStringCritical(env, j_str, c_str); |
DrawString是一个能直接将字符串写到屏幕上的系统调用。除非屏幕显示驱动程序也是在同一虚拟机中运行的Java应用程序,否则DrawString函数将不会无限期地阻止等待垃圾收集发生。
总而言之,您需要考虑一对Get/ReleaseStringCritical调用之间所有可能的阻塞行为。
3.3 访问数组
JNI以不同的方式对待基本数据类型数组和对象数组。基本数据类型数组包含基本数据类型,例如int和boolean。对象数据包含引用类型元素,例如类实例或其他数组。例如下面使用Java编程语言编写的代码中:
1 2 3 4 |
int[] iarr; float[] farr; Object[] oarr; int[][] arr2; |
iarr和farr是基本数据类型数组,而oarr和arr2是对象数组。
在本地代码中访问基本数据类型数组所需要的方法和访问字符串所需要的本地方法类似。让我们看一个基本例子,以下程序调用本地方法sumArray,它将int数组的内容相加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class IntArray { private native int sumArray(int[] arr); public static void main(String[] args) { IntArray p = new IntArray(); int arr[] = new int[10]; for (int i = 0; i < 10; i++) { arr[i] = i; } int sum = p.sumArray(arr); System.out.println("sum = " + sum); } static { System.loadLibrary("IntArray"); } } |
3.3.1 在C中访问数组
数据由jarray引用类型及其“子类型”(如jintArray)表示。正如jstring不是C式字符串一样,jarray也不是C式数据。你不能直接访问jarray引用来完成Java_IntArray_sumArray本地方法的编写。下面的C代码是非法的也不会获取到想要的结果:
1 2 3 4 5 6 7 |
/* This program is illegal! */ JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { int i, sum = 0; for (i = 0; i < 10; i++) { sum += arr[i]; } } |
你应该使用恰当的JNI函数来访问基本数据类型数组中的元素,就像下面展示的正确的例子一样:
1 2 3 4 5 6 7 8 9 |
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { jint buf[10]; jint i, sum = 0; (*env)->GetIntArrayRegion(env, arr, 0, 10, buf); for (i = 0; i < 10; i++) { sum += buf[i]; } return sum; } |
3.3.2 访问基本数据类型数组
前面的例子中使用GetIntArrayRegion函数来复制整型数组中的所有元素到C缓冲区中。第三个参数是需要复制的元素的起始索引,第四个参数表示需要复制的元素的总数。一旦元素存储在C缓冲区中,我们就能够在本地代码中访问他们了。异常检查是不需要的,因为在这个例子中,我们知道数组的长度为10,因此不会引发索引越界问题。
JNI支持相应的SetIntArrayRegion函数,该函数允许本机代码修改int类型的数组元素。 还支持其他原始类型的数组(如boolean、short和float类型)。
JNI支持一系列Get/Release<Type>ArrayElements(博主注<Type> 表示的是基本类型,例如int、float等,因为博客Markdown解析不好,实在没办法弄好,各位看官就将就看了,下同)函数(包括例如Get/ReleaseIntArrayElements),允许本机代码获得对原始数组元素的直接指针。因为底层垃圾收集器不支持固定,所以虚拟机可能会返回指向基本数据类型数组的副本的指针。我们可以使用GetIntArrayElements来重写3.3.1节中的本地代码实现函数(包括例如Get/ReleaseIntArrayElements),允许本机代码获得对原始数组元素的直接指针。因为底层垃圾收集器不支持固定,所以虚拟机可能会返回指向基本数据类型数组的副本的指针。我们可以使用GetIntArrayElements来重写3.3.1节中的本地代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { jint *carr; jint i, sum = 0; carr = (*env)->GetIntArrayElements(env, arr, NULL); if (carr == NULL) { return 0; /* exception occurred */ } for (i=0; i<10; i++) { sum += carr[i]; } (*env)->ReleaseIntArrayElements(env, arr, carr, 0); return sum; } |
GetArrayLength方法返回基本数据类型数组或对象数组中元素的个数。当第一次分配数组的时候,其长度就固定了。
Java 2 SDK 1.2中介绍了Get/ReleasePrimitiveArrayCritical函数。当本地代码访问基本数据类型数组的时候,这些函数允许虚拟机禁用垃圾回收器。程序员注意使用这两个函数必须跟使用Get/ReleaseStringCritical函数一样小心。在Get/ReleasePrimitiveArrayCritical函数对中的本地代码不能随意调用JNI方法,不能进行可能导致死锁的阻塞操作。
3.3.3 访问基本数据类型数组总结
表3.2中列出了访问基本数据类型数据的相关JNI方法,Java 2 JDK 1.2版本中增加了一些增加特定数组操作性能的函数,增加的函数没有提供新的操作,只是做了操作性能的提升而已:
表3.2 访问基本数据类型数组总结
JNI函数 | 描述 | 从哪个版本开始 |
---|---|---|
Get<Type>ArrayRegion\Set<Type>ArrayRegion | 复制基本数据类型数组的内容到C缓冲区或者将C缓冲区的内容复制出来 | JDK 1.1 |
Get<Type>ArrayElements\Release<Type>ArrayElements | 获取一个指向基本数据类型数组内容的指针,可能会返回该数组的副本 | JDK 1.1 |
GetArrayLength | 返回数组中元素的个数 | JDK 1.1 |
New<Type>Array | 创建一个给定长度的数组 | JDK 1.1 |
GetPrimitiveArrayCriticalReleasePrimitiveArrayCritical | 获取一个指向基本数据类型数组内容的指针,可能禁用垃圾收集器或者返回该数组的副本 | Java 2 JDK 1.2 |
3.3.4 选择合适的基本类型数组函数
图3.3表明,在JDK 1.1和Java 2 JDK 1.2版本中,程序员应如何选择恰当的JNI函数来访问基本数据类型数组。
如果你需要将数组内容复制到C缓冲区或者从C缓冲区中将内容复制到数组中,应当使用Get/Set<Type>ArrayRegion家族函数。这些函数会进行边界检查,并且如果有必要的话会抛出ArrayIndexOutOfBoundsException异常。第3.3.1节中的本地方法实现中使用GetIntArrayRegion方法从jarray引用中复制10个元素。
对于小型固定大小的阵列,Get/Set<Type>ArrayRegion几乎总是首选函数,因为C缓冲区可以非常方便的从C堆栈中分配。复制少量数组元素的开销是微不足道的。
Get/Set<Type>ArrayRegion函数允许您指定起始索引和元素数量,因此如果本地代码只需要访问大型数组中的元素的一个子集,则它们是首选函数。
如果没有预分配的C缓冲区,则原始数组的大小不确定,并且本机代码在持有指向数组元素的指针时不发出阻塞调用,请使用Java 2 SDK版本1.2中的Get/ReleasePrimitiveArrayCritical函数。 就像Get/ReleaseStringCritical函数一样,必须非常小心地使用Get/ReleasePrimitiveArrayCritical函数,以避免死锁。
使用Get/Release<Type>ArrayElements系列函数总是安全的。 虚拟机或者返回指向数组元素的直接指针,或者返回一个保存数组元素副本的缓冲区。
3.3.5 访问对象数组
JNI提供了一对单独的函数来访问对象数组。GetObjectArrayElement返回给定索引处的元素,而SetObjectArrayElement更新给定索引处的元素。与原始数组类型的情况不同,您不能一次获取所有对象元素或复制多个对象元素。字符串和数组是引用类型,你可以使用Get/SetObjectArrayElememt访问字符串数组和数组的数组。
下面的代码调用一个本地函数来创建一个int型二维数组,然后打印给数组的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ObjectArrayTest { private static native int[][] initInt2DArray(int size); public static void main(String[] args) { int[][] i2arr = initInt2DArray(3); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { System.out.print(" " + i2arr[i][j]); } System.out.println(); } } static { System.loadLibrary("ObjectArrayTest"); } } |
本地方法initInt2DArray根据给定的大小创建一个二维数组,该本地方法分配和创建二维数组的代码可能如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size) { jobjectArray result; int i; jclass intArrCls = (*env)->FindClass(env, "[I"); if (intArrCls == NULL) { return NULL; /* exception thrown */ } result = (*env)->NewObjectArray(env, size, intArrCls, NULL); if (result == NULL) { return NULL; /* out of memory error thrown */ } for (i = 0; i < size; i++) { jint tmp[256]; /* make sure it is large enough! */ int j; jintArray iarr = (*env)->NewIntArray(env, size); if (iarr == NULL) { return NULL; /* out of memory error thrown */ } for (j = 0; j < size; j++) { tmp[j] = i + j; } (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp); (*env)->SetObjectArrayElement(env, result, i, iarr); (*env)->DeleteLocalRef(env, iarr); } return result; } |
initInt2DArray方法首先调用JNI函数FindClass来获取一个对二维int类型数组的元素类的引用。FindClass的参数“[I”是一个对于与Java编程语言中int[]类型的JNI类描述符(12.3.2节)。如果类型查询失败,FindClass会返回NULL并抛出异常(例如由于缺少类文件或者内存不足的情况)。
下一步,NewObjectArray函数分配一个数组,其元素类型由intArrCls类应用决定。NewObjectArray仅仅分配第一个维度,我们仍然需要填写构成第二个维度的数组元素。Java虚拟机中没有特殊的数据类型来表示多维数组。一个二维数组其实就是一个数组。
创建第二维数组的代码是简单易懂的。NewIntArray分配独立的数组元素,SetIntArrayRegion将tmp缓冲区的内容复制到新分配的一维数组中。完成SetObjectArrayElement调用后,第i个一维数组的第j个元素的值为i+j。执行ObjectArrayTest.main方法可以获得如下输出:
1 2 3 |
0 1 2 1 2 3 2 3 4 |
在循环结尾调用DeleteLocalRef确保虚拟机不会因为保存JNI引用例如iarr,而导致内存耗尽。5.2.1节将解释为什么以及何时需要调用DeleteLocalRef。