JNI (一)

这四种情况下你会用到本书:

1、 在Java程序中复用以前写过的C/C++代码。

2、 自己实现一个java虚拟机

3、 学习不同语言如何进行协作,尤其是如何实现垃圾回收和多线程。

4、 把一个虚拟机实现整合到用C/C++写的程序中。

本书是写给开发者的。JNI1997年第一次发布,本书总结了SUN工程师和大量开发者两年来积累的经验。

本书介绍了JNI的设计思想,对这种思想的理解是使用JNI的各种特性的基础。

本书有一部分是JAVA2平台上面的JNI特征的规范说明。JNI程序员可以把这部分用作一个手册。JVM开发者在实现虚拟机的时候必须遵守这些规范。

JNI的部分设计思想来源于NetscapeJava Runtime Interface(JRI)

 

 

JNIJAVA平台的一个重要特征,使用它我们可以重用以前用C/C++写的大量代码。本书既是一个编程指南也是一个JNI手册。本书共包括三部分:

1、 第二章通过一个简单的例子介绍了JNI。它的对象是对JNI不熟悉的初学者。

2、 3~10章对JNI的特征进行了系统的介绍。我们会举大量的例子来说明JNI的各个特征,这些特征都是JNI中重要且常用的。

3、 11~13章是关于JNI的技术规范。可以把这两章当作一个手册。

本书尽量去满足各类读者的需要。指南面向初学者,手册面向有经验的人和自己实现JNI规范的人。大部分读者可能是用JNI来写程序的开发者。本书会假设你有JAVAC/C++基础。

本章的剩余部分介绍了JNI的背景,扮演的角色和JNI的演化。

1.1 JAVA平台和系统环境(Host Environment

系统环境代指本地操作系统环境,它有自己的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系统环境。比如,一个系统下编译出来的C程序不能在另一个系统中运行。

1.2 JNI扮演的角色

JNI的强大特性使我们在使用JAVA平台的同时,还可以重用原来的本地代码。作为虚拟机实现的一部分,JNI允许JAVA和本地代码间的双向交互。

1.1 JNI的角色

JNI可以这样与本地程序进行交互:

1、 你可以使用JNI来实现“本地方法”(native methods),并在JAVA程序中调用它们。

2、 JNI支持一个“调用接口”(invocation interface),它允许你把一个JVM嵌入到本地程序中。本地程序可以链接一个实现了JVM的本地库,然后使用“调用接口”执行JAVA语言编写的软件模块。例如,一个用C语言写的浏览器可以在一个嵌入式JVM上面执行从网上下载下来的applets

1.3 JNI的副作用

请记住,一旦使用JNIJAVA程序就丧失了JAVA平台的两个优点:

1、 程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。

2、 程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。

一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JAVAC之间的耦合性。

1.4 什么场合下应该使用JNI

当你开始着手准备一个使用JNI的项目时,请确认是否还有替代方案。像上一节所提到的,应用程序使用JNI会带来一些副作用。下面给出几个方案,可以避免使用JNI的时候,达到与本地代码进行交互的效果:

1、 JAVA程序和本地程序使用TCP/IP或者IPC进行交互。

2、 当用JAVA程序连接本地数据库时,使用JDBC提供的API

3、 JAVA程序可以使用分布式对象技术,如JAVA IDL API

这些方案的共同点是,JAVAC 处于不同的线程,或者不同的机器上。这样,当本地程序崩溃时,不会影响到JAVA程序。

下面这些场合中,同一进程内JNI的使用无法避免:

1、 程序当中用到了JAVA API不提供的特殊系统环境才会有的特征。而跨进程操作又不现实。

2、 你可能想访问一些己有的本地库,但又不想付出跨进程调用时的代价,如效率,内存,数据传递方面。

3、 JAVA程序当中的一部分代码对效率要求非常高,如算法计算,图形渲染等。

总之,只有当你必须在同一进程中调用本地代码时,再使用JNI

1.5 JNI的演化

JDK1.0包含了一个本地方法接口,它允许JAVA程序调用C/C++写的程序。许多第三方的程序和JAVA类库,如:java.lang,java.io,java.net等都依赖于本地方法来访问底层系统环境的特征。

不幸的是,JDK1.0中的本地方法有两个主要问题:

1、 本地方法像访问C中的结构(structures)一样访问对象中的字段。尽管如此,JVM规范并没有定义对象怎么样在内存中实现。如果一个给定的JVM实现在布局对象时,和本地方法假设的不一样,那你就不得不重新编写本地方法库。

2、 因为本地方法可以保持对JVM中对象的直接指针,所以,JDK1.0中的本地方法采用了一种保守的GC策略。

JNI的诞生就是为了解决这两个问题,它可以被所有平台下的JVM支持:

1、 每一个VM实现方案可以支持大量的本地代码。

2、 开发工具作者不必处理不同的本地方法接口。

3、 最重要的是,本地代码可以运行在不同的JVM上面。

JDK1.1中第一次支持JNI,但是,JDK1.1仍在使用老风格的本地代码来实现JAVAAPI。这种情况在JDK1.2下被彻底改变成符合标准的写法。

1.6 例子程序

本书包含了大量的代码示例,还教我们如何使用javah来构建JNI程序。

JNIJAVA平台的一个重要特征,使用它我们可以重用以前用C/C++写的大量代码。本书既是一个编程指南也是一个JNI手册。本书共包括三部分:

1、 第二章通过一个简单的例子介绍了JNI。它的对象是对JNI不熟悉的初学者。

2、 3~10章对JNI的特征进行了系统的介绍。我们会举大量的例子来说明JNI的各个特征,这些特征都是JNI中重要且常用的。

3、 11~13章是关于JNI的技术规范。可以把这两章当作一个手册。

本书尽量去满足各类读者的需要。指南面向初学者,手册面向有经验的人和自己实现JNI规范的人。大部分读者可能是用JNI来写程序的开发者。本书会假设你有JAVAC/C++基础。

本章的剩余部分介绍了JNI的背景,扮演的角色和JNI的演化。

1.1 JAVA平台和系统环境(Host Environment

系统环境代指本地操作系统环境,它有自己的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系统环境。比如,一个系统下编译出来的C程序不能在另一个系统中运行。

1.2 JNI扮演的角色

JNI的强大特性使我们在使用JAVA平台的同时,还可以重用原来的本地代码。作为虚拟机实现的一部分,JNI允许JAVA和本地代码间的双向交互。

1.1 JNI的角色

JNI可以这样与本地程序进行交互:

1、 你可以使用JNI来实现“本地方法”(native methods),并在JAVA程序中调用它们。

2、 JNI支持一个“调用接口”(invocation interface),它允许你把一个JVM嵌入到本地程序中。本地程序可以链接一个实现了JVM的本地库,然后使用“调用接口”执行JAVA语言编写的软件模块。例如,一个用C语言写的浏览器可以在一个嵌入式JVM上面执行从网上下载下来的applets

1.3 JNI的副作用

请记住,一旦使用JNIJAVA程序就丧失了JAVA平台的两个优点:

1、 程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。

2、 程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。

一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JAVAC之间的耦合性。

1.4 什么场合下应该使用JNI

当你开始着手准备一个使用JNI的项目时,请确认是否还有替代方案。像上一节所提到的,应用程序使用JNI会带来一些副作用。下面给出几个方案,可以避免使用JNI的时候,达到与本地代码进行交互的效果:

1、 JAVA程序和本地程序使用TCP/IP或者IPC进行交互。

2、 当用JAVA程序连接本地数据库时,使用JDBC提供的API

3、 JAVA程序可以使用分布式对象技术,如JAVA IDL API

这些方案的共同点是,JAVAC 处于不同的线程,或者不同的机器上。这样,当本地程序崩溃时,不会影响到JAVA程序。

下面这些场合中,同一进程内JNI的使用无法避免:

1、 程序当中用到了JAVA API不提供的特殊系统环境才会有的特征。而跨进程操作又不现实。

2、 你可能想访问一些己有的本地库,但又不想付出跨进程调用时的代价,如效率,内存,数据传递方面。

3、 JAVA程序当中的一部分代码对效率要求非常高,如算法计算,图形渲染等。

总之,只有当你必须在同一进程中调用本地代码时,再使用JNI

1.5 JNI的演化

JDK1.0包含了一个本地方法接口,它允许JAVA程序调用C/C++写的程序。许多第三方的程序和JAVA类库,如:java.lang,java.io,java.net等都依赖于本地方法来访问底层系统环境的特征。

不幸的是,JDK1.0中的本地方法有两个主要问题:

1、 本地方法像访问C中的结构(structures)一样访问对象中的字段。尽管如此,JVM规范并没有定义对象怎么样在内存中实现。如果一个给定的JVM实现在布局对象时,和本地方法假设的不一样,那你就不得不重新编写本地方法库。

2、 因为本地方法可以保持对JVM中对象的直接指针,所以,JDK1.0中的本地方法采用了一种保守的GC策略。

JNI的诞生就是为了解决这两个问题,它可以被所有平台下的JVM支持:

1、 每一个VM实现方案可以支持大量的本地代码。

2、 开发工具作者不必处理不同的本地方法接口。

3、 最重要的是,本地代码可以运行在不同的JVM上面。

JDK1.1中第一次支持JNI,但是,JDK1.1仍在使用老风格的本地代码来实现JAVAAPI。这种情况在JDK1.2下被彻底改变成符合标准的写法。

1.6 例子程序

本书包含了大量的代码示例,还教我们如何使用javah来构建JNI程序。

 

本章通过一个简单的例子来示例如何使用JNI。我们写一个JAVA程序,并用它调用一个C函数来打印“Hello World!”。

21 概述

2.1演示了如何使用JAVA程序调用C函数来打印“Hello World!”。这个过程包含下面几步:

1、 创建一个类(HelloWorld.java)声明本地方法。

2、 使用javac编译源文件HollowWorld.java,产生HelloWorld.class。使用javah –jni来生成C头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。

3、 用C代码写函数原型的实现。

4、 把C函数实现编译成一个本地库,创建Hello-World.dll或者libHello-World.so

5、 使用java命令运行HelloWorld程序,类文件HelloWorld.class和本地库(HelloWorld.dll或者libHelloWorld.so)在运行时被加载。

2.1 编写并运行“HelloWorld”程序

本章剩余部分会详细解释这几步。

 

开发者使用JNI时最常问到的是JAVAC/C++之间如何传递数据,以及数据类型之间如何互相映射。本章我们从整数等基本类型和数组、字符串等普通的对象类型开始讲述。至于如何传递任意对象,我们将在下一章中进行讲述。

3.1 一个简单的本地方法

JAVA端源代码如下:

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");

}

}

3.1.1 本地方法的C函数原型

Prompt.getLine方法可以用下面这个C函数来实现:

JNIEXPORT jstring JNICALL

Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);

其中,JNIEXPORT和JNICALL这两个宏(被定义在jni.h)确保这个函数在本地库外可见,并且C编译器会进行正确的调用转换。C函数的名字构成有些讲究,在11.3中会有一个详细的解释。

3.1.2 本地方法参数

第一个参数JNIEnv接口指针,指向一个个函数表,函数表中的每一个入口指向一个JNI函数。本地方法经常通过这些函数来访问JVM中的数据结构。图3.1演示了JNIEnv这个指针:

3.1 JNIEnv接口指针

第二个参数根据本地方法是一个静态方法还是实例方法而有所不同。本地方法是一个静态方法时,第二个参数代表本地方法所在的类;本地方法是一个实例方法时,第二个参数代表本地方法所在的对象。我们的例子当中,Java_Prompt_getLine是一个实例方法,因此jobject参数指向方法所在的对象。

3.1.3 类型映射

本地方法声明中的参数类型在本地语言中都有对应的类型。JNI定义了一个C/C++类型的集合,集合中每一个类型对应于JAVA中的每一个类型。

JAVA中有两种类型:基本数据类型(int,float,char等)和引用类型(类,对象,数组等)。

JNI对基本类型和引用类型的处理是不同的。基本类型的映射是一对一的。例如JAVA中的int类型直接对应C/C++中的jint(定义在jni.h中的一个有符号 32位整数)。12.1.1包含了JNI中所有基本类型的定义。

JNIJAVA中的对象当作一个C指针传递到本地方法中,这个指针指向JVM中的内部数据结构,而内部数据结构在内存中的存储方式是不可见的。本地代码必须通过在JNIEnv中选择适当的JNI函数来操作JVM中的对象。例如,对于java.lang.String对应的JNI类型是jstring,但本地代码只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。

所有的JNI引用都是jobject类型,对了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类型。这些子类型和JAVA中常用的引用类型相对应。例如,jstring表示字符串,jobjectArray表示对象数组。

3.2 访问字符串

Java_Prompt_getLine接收一个jstring类型的参数promptjstring类型指向JVM内部的一个字符串,和常规的C字符串类型char*不同。你不能把jstring当作一个普通的C字符串。

3.2.1 转换为本地字符串

本地代码中,必须使用合适的JNI函数把jstring转化为C/C++字符串。JNI支持字符串在UnicodeUTF-8两种编码之间转换。Unicode字符串代表了16-bit的字符集合。UTF-8字符串使用一种向上兼容7-bit ASCII字符串的编码协议。UTF-8字符串很像NULL结尾的C字符串,在包含非ASCII字符的时候依然如此。所有的7-bitASCII字符的值都在1~127之间,这些值在UTF-8编码中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符(16-bitUnicode值)。

函数Java_Prompt_getLine通过调用JNI函数GetStringUTFChars来读取字符串的内容。GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8格式的C字符串。如何你确信原始字符串数据只包含7-bit ASCII字符,你可以把转化后的字符串传递给常规的C库函数使用,如printf。我们会在8.2中讨论如何处理非ASCII字符串。

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; /* OutOfMemoryError already thrown */

}

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

不要忘记检查GetStringUTFChars。因为JVM需要为新诞生的UTF-8字符串分配内存,这个操作有可能因为内存太少而失败。失败时,GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常(对异常的处理在第6章)。这些JNI抛出的异常与JAVA中的异常是不同的。一个由JNI抛出的未决的异常不会改变程序执行流,因此,我们需要一个显示的return语句来跳过C函数中的剩余语句。Java_Prompt_getLine函数返回后,异常会在Prompt.main(Prompt.getLine这个发生异常的函数的调用者)中抛出,

3.2.2 释放本地字符串资源

从GetStringUTFChars中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉JVM这个UTF-8字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。

3.2.3 构造新的字符串

你可以通过JNI函数NewStringUTF在本地方法中创建一个新的java.lang.String字符串对象。这个新创建的字符串对象拥有一个与给定的UTF-8编码的C类型字符串内容相同的Unicode编码字符串。

如果一个VM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回一个NULL。在这个例子中,我们不必检查它的返回值,因为本地方法会立即返回。如果NewStringUTF失败,OutOfMemoryError这个异常会被在Prompt.main(本地方法的调用者)中抛出。如果NeweStringUTF成功,它会返回一个JNI引用,这个引用指向新创建的java.lang.String对象。这个对象被Prompt.getLine返回然后被赋值给Prompt.main中的本地input。

3.2.4 其它JNI字符串处理函数

JNI支持许多操作字符串的函数,这里做个大致介绍。

GetStringChars和ReleaseStringChars获取以Unicode格式编码的字符串。当操作系统支持Unicode编码的字符串时,这些方法很有用。

UTF-8字符串以\0结尾,而Unicode字符串不是。如果jstring指向一个Unicode编码的字符串,为了得到这个字符串的长度,可以调用GetStringLength。如果一个jstring指向一个UTF-8编码的字符串,为了得到这个字符串的字节长度,可以调用标准C函数strlen。或者直接对jstring调用JNI函数GetStringUTFLength,而不用管jstring指向的字符串的编码格式。

GetStringChars和GetStringUTFChars函数中的第三个参数需要更进一步的解释:

const jchar *

GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

当从JNI函数GetStringChars中返回得到字符串B时,如果B是原始字符串java.lang.String的拷贝,则isCopy被赋值为JNI_TRUE。如果B和原始字符串指向的是JVM中的同一份数据,则isCopy被赋值为JNI_FALSE。当isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破JAVA语言中字符串不可变的规则。

通常,因为你不必关心JVM是否会返回原始字符串的拷贝,你只需要为isCopy传递NULL作为参数。

JVM是否会通过拷贝原始Unicode字符串来生成UTF-8字符串是不可以预测的,程序员最好假设它会进行拷贝,而这个操作是花费时间和内存的。一个典型的JVM会在heap上为对象分配内存。一旦一个JAVA字符串对象的指针被传递给本地代码,GC就不会再碰这个字符串。换言之,这种情况下,JVM必须pin这个对象。可是,大量地pin一个对象是会产生内存碎片的,因为,虚拟机会随意性地来选择是复制还是直接传递指针。

当你不再使用一个从GetStringChars得到的字符串时,不管JVM内部是采用复制还是直接传递指针的方式,都不要忘记调用ReleaseStringChars。根据方法GetStringChars是复制还是直接返回指针,ReleaseStringChars会释放复制对象时所占的内存,或者unpin这个对象。

3.2.5 JDK1.2中关于字符串的新JNI函数

为了提高JVM返回字符串直接指针的可能性,JDK1.2中引入了一对新函数,Get/ReleaseStringCritical。表面上,它们和Get/ReleaseStringChars函数差不多,但实际上这两个函数在使用有很大的限制。

使用这两个函数时,你必须两个函数中间的代码是运行在"critical region"(临界区)的,即,这两个函数中间的本地代码不能调用任何会让线程阻塞或等待JVM中的其它线程的本地函数或JNI函数。

有了这些限制, JVM就可以在本地方法持有一个从GetStringCritical得到的字符串的直接指针时禁止GC。当GC被禁止时,任何线程如果触发GC的话,都会被阻塞。而Get/ReleaseStringCritical这两个函数中间的任何本地代码都不可以执行会导致阻塞的调用或者为新对象在JVM中分配内存。否则,JVM有可能死锁,想象一下这样的场景中:

1、 只有当前线程触发的GC完成阻塞并释放GC时,由其它线程触发的GC才可能由阻塞中释放出来继续运行。

2、 在这个过程中,当前线程会一直阻塞。因为任何阻塞性调用都需要获取一个正被其它线程持有的锁,而其它线程正等待GC

Get/ReleaseStringCritical的交迭调用是安全的,这种情况下,它们的使用必须有严格的顺序限制。而且,我们一定要记住检查是否因为内存溢出而导致它的返回值是NULL。因为JVM在执行GetStringCritical这个函数时,仍有发生数据复制的可能性,尤其是当JVM内部存储的数组不连续时,为了返回一个指向连续内存空间的指针,JVM必须复制所有数据。

总之,为了避免死锁,在Get/ReleaseStringCritical之间不要调用任何JNI函数。Get/ReleaseStringCritical和 Get/ReleasePrimitiveArrayCritical这两个函数是可以的。

下面代码演示了这对函数的正确用法:

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);

JNI不支持Get/ReleaseStringUTFCritical,因为这样的函数在进行编码转换时很可能会促使JVM对数据进行复制,因为JVM内部表示字符串一般都是使用Unicode的。

JDK1.2还一对新增的函数:GetStringRegion和GetStringUTFRegion。这对函数把字符串复制到一个预先分配的缓冲区内。Prompt.getLine这个本地方法可以用GetStringUTFRegion重新实现如下:

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这个函数会做越界检查,如果必要的话,会抛出异常StringIndexOutOfBoundsException。这个方法与GetStringUTFChars比较相似,不同的是,GetStringUTFRegion不做任何内存分配,不会抛出内存溢出异常。

3.2.6 JNI字符串操作函数总结

对于小字符串来说,Get/SetStringRegion和Get/SetString-UTFRegion这两对函数是最佳选择,因为缓冲区可以被编译器提前分配,而且永远不会产生内存溢出的异常。当你需要处理一个字符串的一部分时,使用这对函数也是不错的,因为它们提供了一个开始索引和子字符串的长度值。另外,复制少量字符串的消耗是非常小的。

在使用GetStringCritical时,必须非常小心。你必须确保在持有一个由GetStringCritical获取到的指针时,本地代码不会在JVM内部分配新对象,或者做任何其它可能导致系统死锁的阻塞性调用。

下面的例子演示了使用GetStringCritical时需要注意的一些地方:

/* 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);

上面代码的问题在于,GC被当前线程禁止的情况下,向一个文件写数据不一定安全。例如,另外一个线程T正在等待从文件fd中读取数据。假设操作系统的规则是fprintf会等待线程T完成所有对文件fd的数据读取操作,这种情况下就可能会产生死锁:线程T从文件fd中读取数据是需要缓冲区的,如果当前没有足够内存,线程T就会请求GC来回收一部分,GC一旦运行,就只能等到当前线程运行ReleaseStringCritical时才可以。而ReleaseStringCritical只有在fprintf调用返回时才会被调用。而fprintf这个调用,会一直等待线程T完成文件读取操作。

3.3 访问数组

JNI在处理基本类型数组和对象数组上面是不同的。对象数组里面是一些指向对象实例或者其它数组的引用。

本地代码中访问JVM中的数组和访问JVM中的字符串有些相似。看一个简单的例子。下面的程序调用了一个本地方法sumArray,这个方法对一个int数组里面的元素进行累加:

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 在本地代码中访问数组

数组的引用类型是一般是jarray或者或者jarray的子类型jintArray。就像jstring不是一个C字符串类型一样,jarray也不是一个C数组类型。所以,不要直接访问jarray。你必须使用合适的JNI函数来访问基本数组元素:

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函数来把一个int数组中的所有元素复制到一个C缓冲区中,然后我们在本地代码中通过C缓冲区来访问这些元素。

JNI支持一个与GetIntArrayRegion相对应的函数SetIntArrayRegion。这个函数允许本地代码修改所有的基本类型数组中的元素。

JNI支持一系列的Get/Release<Type>ArrayElement函数,这些函数允许本地代码获取一个指向基本类型数组的元素的指针。由于GC可能不支持pin操作,JVM可能会先对原始数据进行复制,然后返回指向这个缓冲区的指针。我们可以重写上面的本地方法实现:

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这个函数返回数组中元素的个数,这个值在数组被首次分配时确定下来。

JDK1.2引入了一对函数:Get/ReleasePrimitiveArrayCritical。通过这对函数,可以在本地代码访问基本类型数组元素的时候禁止GC的运行。但程序员使用这对函数时,必须和使用Get/ReleaseStringCritical时一样的小心。在这对函数调用的中间,同样不能调用任何JNI函数,或者做其它可能会导致程序死锁的阻塞性操作。

3.3.3 操作基本类型数组的JNI函数的总结

如果你想在一个预先分配的C缓冲区和内存之间交换数据,应该使用Get/Set</Type>ArrayRegion系列函数。这些函数会进行越界检查,在需要的时候会有可能抛出ArrayIndexOutOfBoundsException异常。

对于少量的、固定大小的数组,Get/Set<Type>ArrayRegion是最好的选择,因为C缓冲区可以在Stack(栈)上被很快地分配,而且复制少量数组元素的代价是很小的。这对函数的另外一个优点就是,允许你通过传入一个索引和长度来实现对子字符串的操作。

如果你没有一个预先分配的C缓冲区,并且原始数组长度未定,而本地代码又不想在获取数组元素的指针时阻塞的话,使用Get/ReleasePrimitiveArrayCritical函数对。就像Get/ReleaseStringCritical函数对一样,这对函数很小心地使用,以避免死锁。

Get/Release<type>ArrayElements系列函数永远是安全的。JVM会选择性地返回一个指针,这个指针可能指向原始数据也可能指向原始数据复制。

3.3.5 访问对象数组

JNI提供了一个函数对来访问对象数组。GetObjectArrayElement返回数组中指定位置的元素,而SetObjectArrayElement修改数组中指定位置的元素。与基本类型的数组不同的是,你不能一次得到所有的对象元素或者一次复制多个对象元素。字符串和数组都是引用类型,你要使用Get/SetObjectArrayElement来访问字符串数组或者数组的数组。

下面的例子调用了一个本地方法来创建一个二维的int数组,然后打印这个数组的内容:

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创建了一个给定大小的二维数组。执行分配和初始化数组任务的本地方法可以是下面这样子的:

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;

}

函数newInt2DArray首先调用JNI函数FindClass来获得一个int型二维数组类的引用,传递给FindClass的参数“[I”是JNI class descriptor(JNI类型描述符),它对应着JVM中的int[]类型。如果类加载失败的话,FindClass会返回NULL,然后抛出一个异常。

接下来,NewObjectArray会分配一个数组,这个数组里面的元素类型用intArrCls类引用来标识。函数NewObjectArray只能分配第一维,JVM没有与多维数组相对应的数据结构。一个二维数组实际上就是一个简单的数组的数组。

创建第二维数据的方式非常直接,NewInt-Array为每个数组元素分配空间,然后SetIntArrayRegion把tmp[]缓冲区中的内容复制到新分配的一维数组中去。

在循环最后调用DeleteLocalRef,确保JVM释放掉iarr这个JNI引用。

 

现在,你知道了如何通过JNI来访问JVM中的基本类型数据和字符串、数组这样的引用类型数据,下一步就是学习怎么样和JVM中任意对象的字段和方法进行交互。比如从本地代码中调用JAVA中的方法,也就是通常说的来自本地方法中的callbacks(回调)。

我们从进行字段访问和方法回调时需要的JNI函数开始讲解。本章的稍后部分我们会讨论怎么样通过一些cache(缓存)技术来优化这些操作。在最后,我们还会讨论从本地代码中访问字段和回调方法时的效率问题。

4.1 访问字段

JAVA支持两种field(字段),每一个对象的实例都有一个对象字段的复制;所有的对象共享一个类的静态字段。本地方法使用JNI提供的函数可以获取和修改这两种字段。先看一个从本地代码中访问对象字段的例子:

class InstanceFieldAccess {

private String s;

private native void accessField();

public static void main(String args[]) {

InstanceFieldAccess c = new InstanceFieldAccess();

c.s = "abc";

c.accessField();

System.out.println("In Java:");

System.out.println(" c.s = \"" + c.s + "\"");

}

static {

System.loadLibrary("InstanceFieldAccess");

}

}

InstanceFieldAccess这个类定义了一个对象字段s。main方法创建了一个对象并设置s的值,然后调用本地方法InstanceFieldAccess.accessField在本地代码中打印s的值,并把它修改为一个新值。本地方法返回后,JAVA中把这个值再打印一次,可以看出来,字段s的值已经被改变了。下面是本地方法的实现:

JNIEXPORT void JNICALL

Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)

{

jfieldID fid; /* store the field ID */

jstring jstr;

const char *str;

/* Get a reference to obj's class */

jclass cls = (*env)->GetObjectClass(env, obj);

printf("In C:\n");

/* Look for the instance field s in cls */

fid = (*env)->GetFieldID(env, cls, "s",

"Ljava/lang/String;");

if (fid == NULL) {

return; /* failed to find the field */

}

/* Read the instance field s */

jstr = (*env)->GetObjectField(env, obj, fid);

str = (*env)->GetStringUTFChars(env, jstr, NULL);

if (str == NULL) {

return; /* out of memory */

}

printf(" c.s = \"%s\"\n", str);

(*env)->ReleaseStringUTFChars(env, jstr, str);

/* Create a new string and overwrite the instance field */

jstr = (*env)->NewStringUTF(env, "123");

if (jstr == NULL) {

return; /* out of memory */

}

(*env)->SetObjectField(env, obj, fid, jstr);

}

运行程序,得到输出为:

In C:

c.s = "abc"

In Java:

c.s = "123"

4.1.1 访问一个对象字段的流程

为了访问一个对象的实例字段,本地方法需要做两步:

首先,通过在类引用上调用GetFieldID获取field ID(字段ID)、字段名字和字段描述符:

Fid=(*env)->GetFieldID(env,cls,”s”,”Ljava/lang/String;”);

上例中的代码通过在对象引用obj上调用GetObjectClass获取到类引用。一旦获取到字段ID,你就可以把对象和字段ID作为参数来访问字段:

Jstr=(*env)->GetObjectField(env,obj,fid);

因为字符串和数组是特殊的对象,所以我们使用GetObjectField来访问字符串类型的实例字段。除了Get/SetObjectFieldJNI还支持其它如GetIntFieldSetFloatField等用来访问基本类型字段的函数。

4.1.2 字段描述符

在上一节我们使用过一个特殊的C字符串“Ljava/lang/String”来代表一个JVM中的字段类型。这个字符串被称为JNI field descriptor(字段描述符)。

字符串的内容由字段被声明的类型决定。例如,使用“I”来表示一个int类型的字段,“F”来表示一个float类型的字段,“D”来表示一个double类型的字段,“Z”来表示一个boolean类型的字段等等。

java.lang.String这样的引用类型的描述符都是以L开头,后面跟着一个JNI类描述符,以分号结尾。一个JAVA类的全名中的包名分隔符“.”被转化成“/”。因此,对于一个字段类型的字段来说,它的描述符是“Ljava/lang/String”。

数组的描述符中包含“]”字符,后面会跟着数组类型的描述符,如“[I”是int[]类型的字段的描述符。12.3.3详细介绍了各种类型的字段描述以及他们代表的JAVA类型。

你可以使用javap工具来生成字段描述符。

4.1.3 访问静态字段

访问静态字段和访问实例字段相似,看下面这个InstanceFieldAccess例子的变形:

class StaticFielcdAccess {

private static int si;

private native void accessField();

public static void main(String args[]) {

StaticFieldAccess c = new StaticFieldAccess();

StaticFieldAccess.si = 100;

c.accessField();

System.out.println("In Java:");

System.out.println(" StaticFieldAccess.si = " + si);

}

static {

System.loadLibrary("StaticFieldAccess");

}

}

StaticFieldAccess这个类包含一个静态字段si,main方法创建了一个对象,初始化静态字段,然后调用本地方法StaticFieldAccess.accessField在本地代码中打印静态字段中的值,然后设置新的值,为了演示这个值确实被改变了,在本地方法返回后,JAVA中再次这个静态字段的值。

下面是本地方法StaticFieldAccess.accessField的实现:

JNIEXPORT void JNICALL

Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj)

{

jfieldID fid; /* store the field ID */

jint si;

/* Get a reference to obj's class */

jclass cls = (*env)->GetObjectClass(env, obj);

printf("In C:\n");

/* Look for the static field si in cls */

fid = (*env)->GetStaticFieldID(env, cls, "si", "I");

if (fid == NULL) {

return; /* field not found */

}

/* Access the static field si */

si = (*env)->GetStaticIntField(env, cls, fid);

printf(" StaticFieldAccess.si = %d\n", si);

(*env)->SetStaticIntField(env, cls, fid, 200);

}

运行程序可得到输出结果:

In C:

StaticFieldAccess.si = 100

In Java:

StaticFieldAccess.si = 200

访问静态字段和对象实例字段的不同点:

1、 访问静态字段使用GetStaticFieldID,而访问对象的实例字段使用GetFieldID,但是,这两个方法都有相同的返回值类型:jfieldID。

4.2 调用方法

JAVA中有几种不同类型的方法,实例方法必须在一个类的某个对象实例上面调用。而静态方法可以在任何一个对象实例上调用。对于构建方法的调用我们推迟到下一节。

JNI支持一系列完整的函数让你可以在本地代码中回调JAVA方法,下面例子演示了如何从本地代码中调用一个JAVA中的实例方法:

class InstanceMethodCall {

private native void nativeMethod();

private void callback() {

System.out.println("In Java");

}

public static void main(String args[]) {

InstanceMethodCall c = new InstanceMethodCall();

c.nativeMethod();

}

static {

System.loadLibrary("InstanceMethodCall");

}

}

下面的是本地方法的实现:

JNIEXPORT void JNICALL

Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)

{

jclass cls = (*env)->GetObjectClass(env, obj);

jmethodID mid =

(*env)->GetMethodID(env, cls, "callback", "()V");

if (mid == NULL) {

return; /* method not found */

}

printf("In C\n");

(*env)->CallVoidMethod(env, obj, mid);

}

运行程序,得到如下输出:

In C

In Java

4.2.1 调用实例方法

本地方法Java_InstanceMethodCall_nativeMethod的实现演示了在本地代码中调用JAVA方法的两步:

1、 本地方法首先调用JNI函数GetMethodID。这个函数在指定的类中寻找相应的方法。这个寻找过程是基于方法描述符的。如果方法不存在,GetMethodID返回NULL。这时,立即从本地方法中返回,并引发一个NoSuchMethodError错误。

2、 本地方法通过调用CallVoidMethod来调用返回值为void的实例方法。

除了CallVoidMethod这个函数以外,JNI也支持对返回值为其它类型的方法的调用。如果你调用的方法返回值类型为int,你的本地方法会使用CallIntMethod。类似地,你可以调用CallObjectMethod来调用返回值为java.lang.String、数组等对象类型的方法。

你也可以使用Call<Type>Method系列的函数来调用接口方法。你必须从接口类型中获取方法ID,下面的代码演示了如何在java.lang.Thread实例上面调用Runnable.run方法:

jobject thd = ...; /* a java.lang.Thread instance */

jmethodID mid;

jclass runnableIntf =

(*env)->FindClass(env, "java/lang/Runnable");

if (runnableIntf == NULL) {

... /* error handling */

}

mid = (*env)->GetMethodID(env, runnableIntf, "run", "()V");

if (mid == NULL) {

... /* error handling */

}

(*env)->CallVoidMethod(env, thd, mid);

... /* check for possible exceptions */

3.3.5中,我们使用FindClass来获取一个类的引用,在这里,我们可以学到如何获取一个接口的引用。

4.2.2 生成方法描述符

JNI中描述字段使用字段描述符,描述方法同样有方法描述符。一个方法描述符包含参数类型和返回值类型。参数类型出现在前面,并由一对圆括号将它们括起来,参数类型按它们在方法声明中出现的顺序被列出来,并且多个参数类型之间没有分隔符。如果一个方法没有参数,被表示为一对空圆括号。方法的返回值类型紧跟参数类型的右括号后面。

例如,“(IV”表示这个方法的一个参数类型为int,并且有一个void类回值。“()D”表示这个方法没有参数,返回值类型为double

方法描述符中可能会包含类描述符(12.3.2),如方法native private String getLine(String);的描述符为:“(Ljava/lang/String;)Ljava/lang/String;

数组类型的描述符以“[”开头,后面跟着数组元素类型的描述符。如,public static void main(String[] args);的描述符是:"([Ljava/lang/String;)V"

12.3.4详细描述了怎么样生成一个JNI方法描述符。同样,你可以使用javap工具来打印出JNI方法描述符。

4.2.3 调用静态方法

前一个例子演示了一个本地方法怎样调用实例方法。类似地,本地方法中同样可以调用静态方法:

1、 通过GetStaticMethodID获取方法ID。对应于调用实例方法时的GetMethodID。

2、 传入类、方法ID、参数,并调用提供静态方法调用功能的JNI系列函数中的一个,如:CallStaticVoidMethod,CallStaticBooleanMethod等。

调用静态方法和调用实例方法的JNI函数有一个很大的不同,前者第二个参数是类引用,后者是对象实例的引用。

在JAVA访问一个静态方法可以通过类,也可以通过对象实例。而JNI的规定是,在本地代码中回调JAVA中的静态方法时,必须指定一个类引用才行。下面的例子演示了这个用法:

class StaticMethodCall {

private native void nativeMethod();

private static void callback() {

System.out.println("In Java");

}

public static void main(String args[]) {

StaticMethodCall c = new StaticMethodCall();

c.nativeMethod();

}

static {

System.loadLibrary("StaticMethodCall");

}

}

下面是本地方法的实现:

JNIEXPORT void JNICALL

Java_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj)

{

jclass cls = (*env)->GetObjectClass(env, obj);

jmethodID mid =

(*env)->GetStaticMethodID(env, cls, "callback", "()V");

if (mid == NULL) {

return; /* method not found */

}

printf("In C\n");

(*env)->CallStaticVoidMethod(env, cls, mid);

}

当调用CallStaticVoidMethod时,确保你传入的是类引用cls而不是对象引用obj。运行程序,输出为:

In C

In Java

4.2.4 调用父类的实例方法

如果一个方法被定义在父类中,在子类中被覆盖,你也可以调用这个实例方法。JNI提供了一系列完成这些功能的函数:CallNonvirtual<Type>Method。为了调用一个定义在父类中的实例方法,你必须遵守下面的步骤:

1、 使用GetMethodID从一个指向父类的引用当中获取方法ID。

2、 传入对象、父类、方法ID和参数,并调用CallNonvirtualVoidMethod、CallNonvirtualBooleanMethod等一系列函数中的一个。

这种调用父类实例方法的情况其实很少遇到,通常在JAVA中可以很简单地做到:super.f();

CallNonvirtualVoidMethod也可以被用来调用父类的构造函数。这个在下节就会讲到。

4.3 调用构造函数

JNI中,构造函数可以和实例方法一样被调用,调用方式也相似。传入“<init>”作为方法名,“V”作为返回类型。你可以通过向JNI函数NewObject传入方法来调用构造函数。下面的代码实现了与JNI函数NewString相同的功能:把存储在C缓冲区内的Unicode编码的字符序列,创建成一个java.lang.String对象:

jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)

{

jclass stringClass;

jmethodID cid;

jcharArray elemArr;

jstring result;

stringClass = (*env)->FindClass(env, "java/lang/String");

if (stringClass == NULL) {

return NULL; /* exception thrown */

}

/* Get the method ID for the String(char[]) constructor */

cid = (*env)->GetMethodID(env, stringClass,

"<init>", "([C)V");

if (cid == NULL) {

return NULL; /* exception thrown */

}

/* Create a char[] that holds the string characters */

elemArr = (*env)->NewCharArray(env, len);

if (elemArr == NULL) {

return NULL; /* exception thrown */

}

(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);

/* Construct a java.lang.String object */

result = (*env)->NewObject(env, stringClass, cid, elemArr);

/* Free local references */

(*env)->DeleteLocalRef(env, elemArr);

(*env)->DeleteLocalRef(env, stringClass);

return result;

}

上面这个本地方法有些复杂,需要详细解释一下。首先,FindClass返回一个java.lang.String类的引用,接着,GetMethodID返回构造函数String(char[] chars)的方法ID。我们调用NewCharArray分配一个字符数组来保存字符串元素。JNI函数NewObject调用方法ID所标识的构造函数。NewObject函数需要的参数有:类的引用、构造方法的ID、构造方法需要的参数。

DeleteLocalRef允许VM释放被局部引用elemArr和stringClass引用的资源。5.2.1中详细描述了调用DeleteLocalRef的时机和原因。

这个例子引出了一个问题,既然我们可以利用JNI函数来实现相同的功能,为什么JNI还需要NewString这样的内置函数?原因是,内置函数的效率远高于在本地代码里面调用构造函数的API。而字符串又是最常用到的对象类型,因此需要在JNI中给予特殊的支持。

你也可以做到通过CallNonvirtualVoidMethod函数来调用构造函数。这种情况下,本地代码必须首先通过调用AllocObject函数创建一个未初始化的对象。上面例子中的result = (*env)->NewObject(env, stringClass, cid, elemArr);可以被如下代码替换:

result = (*env)->AllocObject(env, stringClass);

if (result) {

(*env)->CallNonvirtualVoidMethod(env, result, stringClass,

cid, elemArr);

/* we need to check for possible exceptions */

if ((*env)->ExceptionCheck(env)) {

(*env)->DeleteLocalRef(env, result);

result = NULL;

}

}

AllocObject创建了一个未初始化的对象,使用时一定要非常小心,确保一个对象上面,构造函数最多被调用一次。本地代码不应该在一个对象上面调用多次构造函数。有时,你可能会发现创建一个未初始化的对象然后一段时间以后再调用构造函数的方式是很有用的。尽管如此,大部分情况下,你应该使用NewObject,尽量避免使用容易出错的AllocObject/CallNonvirtualVoidMethod方法。

4.4 缓存字段ID和方法ID

获取字段ID和方法ID时,需要用字段、方法的名字和描述符进行一个检索。检索过程相对比较费时,因此本节讨论用缓存技术来减少这个过程带来的消耗。缓存字段ID和方法ID的方法主要有两种。两种区别主要在于缓存发生的时刻,是在字段ID和方法ID被使用的时候,还是定义字段和方法的类静态初始化的时候。

4.4.1 使用时缓存

字段ID和方法ID可以在字段的值被访问或者方法被回调的时候缓存起来。下面的代码中把字段ID存储在静态变量当中,这样当本地方法被重复调用时,不必重新搜索字段ID:

JNIEXPORT void JNICALL

Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)

{

static jfieldID fid_s = NULL; /* cached field ID for s */

jclass cls = (*env)->GetObjectClass(env, obj);

jstring jstr;

const char *str;

if (fid_s == NULL) {

fid_s = (*env)->GetFieldID(env, cls, "s",

"Ljava/lang/String;");

if (fid_s == NULL) {

return; /* exception already thrown */

}

}

printf("In C:\n");

jstr = (*env)->GetObjectField(env, obj, fid_s);

str = (*env)->GetStringUTFChars(env, jstr, NULL);

if (str == NULL) {

return; /* out of memory */

}

printf(" c.s = \"%s\"\n", str);

(*env)->ReleaseStringUTFChars(env, jstr, str);

jstr = (*env)->NewStringUTF(env, "123");

if (jstr == NULL) {

return; /* out of memory */

}

(*env)->SetObjectField(env, obj, fid_s, jstr);

}

由于多个线程可能同时访问这个本地方法,上面方法中的代码很可能会导致混乱,其实没事,多个线程计算的ID其实是相同的。

同样的思想,我们也可以缓存java.lang.String的构造方法的ID:

jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)

{

jclass stringClass;

jcharArray elemArr;

static jmethodID cid = NULL;

jstring result;

stringClass = (*env)->FindClass(env, "java/lang/String");

if (stringClass == NULL) {

return NULL; /* exception thrown */

}

/* Note that cid is a static variable */

if (cid == NULL) {

/* Get the method ID for the String constructor */

cid = (*env)->GetMethodID(env, stringClass,

"<init>", "([C)V");

if (cid == NULL) {

return NULL; /* exception thrown */

}

}

/* Create a char[] that holds the string characters */

elemArr = (*env)->NewCharArray(env, len);

if (elemArr == NULL) {

return NULL; /* exception thrown */

}

(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);

/* Construct a java.lang.String object */

result = (*env)->NewObject(env, stringClass, cid, elemArr);

/* Free local references */

(*env)->DeleteLocalRef(env, elemArr);

(*env)->DeleteLocalRef(env, stringClass);

return result;

}

当MyNewString方法第一次被调用时,我们计算java.lang.String的构造方法的ID,并存储在静态变量cid中。

4.4.2 类的静态初始化过程中缓存字段和方法ID

我们在使用时缓存字段和方法的ID的话,每次本地方法被调用时都要检查ID是否已经被缓存。许多情况下,在字段ID和方法ID被使用前就初始化是很方便的。VM在调用一个类的方法和字段之前,都会执行类的静态初始化过程,所以在静态初始化该类的过程中计算并缓存字段ID和方法ID是个不错的选择。

例如,为了缓存InstanceMethodCall.callback的方法ID,我们引入了一个新的本地方法initIDs,这个方法在InstanceMethodCall的静态初始化过程中被调用。代码如下:

class InstanceMethodCall {

private static native void initIDs();

private native void nativeMethod();

private void callback() {

System.out.println("In Java");

}

public static void main(String args[]) {

InstanceMethodCall c = new InstanceMethodCall();

c.nativeMethod();

}

static {

System.loadLibrary("InstanceMethodCall");

initIDs();

}

}

与4.2节中的代码相比,上面这段代码多了两行,initIDs方法简单地计算并缓存方法ID:

jmethodID MID_InstanceMethodCall_callback;

JNIEXPORT void JNICALL

Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls)

{

MID_InstanceMethodCall_callback =

(*env)->GetMethodID(env, cls, "callback", "()V");

}

VM进行静态初始化时在调用任何方法前调用initIDs,这样方法ID就被缓存了全局变量中,本地方法的实现就不必再进行ID计算:

JNIEXPORT void JNICALL

Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)

{

printf("In C\n");

(*env)->CallVoidMethod(env, obj,

MID_InstanceMethodCall_callback);

}

4.4.3 两种缓存ID的方式之间的对比

如果JNI程序员不能控制方法和字段所在的类的源码的话,在使用时缓存是个合理的方案。例如在MyNewString当中,我们不能在String类中插入一个initIDs方法。

比起静态初始时缓存来说,使用时缓存有一些缺点:

1、 使用时缓存的话,每次使用时都要检查一下。

2、 方法ID和字段ID在类被unload时就会失效,如果你在使用时缓存ID,你必须确保只要本地代码依赖于这个ID的值,那么这个类不被会unload(下一章演示了如何通过使用JNI函数创建一个类引用来防止类被unload)。另一方面,如果缓存发生在静态初始化时,当类被unload和reload时,ID会被重新计算。

因此,尽可能在静态初始化时缓存字段ID和方法ID。

4.5 JNI操作JAVA中的字段和方法时的效率

学完了如何缓存ID来提高效率后,你可能会对使用JNI访问java字段和方法的效率不太明白,native/java比起java/native和java/java来的话,效率如何呢?

当然,这取决于VM的实现。我们不能给出在大范围的VM上通用的数据,但我们可以通过分析本地方法回调java方法和JNI操作字段以及方法的过程来给出一个大致的概念。

我们从比较java/native和java/java的效率开始。java/native调用比java/java要慢,主要有以下几个原因:

1、 java/native比起JVM内部的java/java来说有一个调用转换过程,在把控制权和入口切换给本地方法之前,VM必须做一些额外的操作来创建参数和栈帧。

2、 对VM来说,对方法调用进行内联比较容易,而内联java/native方法要难得多。

据我们的估计,VM进行java/native调用时的消耗是java/java的2~3倍。当然VM可以进行一些调整,使用java/native的消耗接近或者等于java/java的消耗。

技术上来讲,native/java调用和java/native是相似的。但实际上native/java调用很少见,VM通常不会优化native/java这种回调方式。多数VM中,native/java调用的消耗可以达到java/java调用的10倍。

使用JNI访问字段的花费取决于通过JNIEnv进行调用的消耗。以废弃一个对象引用来说,本地代码必须依赖于特定的JNI函数才能做到,而这个依赖是必须的,它把本地代码和VM中对象的内部形式很好地隔离开。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值