Java编程思想第4版[中文版](PDF格式)-第167部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
JNIEnv_结构被定义成一个类。这个类包含了大量内嵌函数。通过一种简单而且熟悉的语法,这些函数让我们
可以从容访问JNI 函数。例如,前例包含了下面这行代码:
(*jEnv)…》ReleaseStringUTFChars(jEnv; jMsg;msg);
它在C++里可改写成下面这个样子:
jEnv…》ReleaseStringUTFChars(jMsg;msg);
大家可注意到自己不再需要同时撤消对jEnv 的两个引用,相同的指针不再作为第一个参数传递给JNI 函数调
用。在这些例子剩下的地方,我会使用C++风格的代码。
1。 访问Java 字串
作为访问JNI 函数的一个例子,请思考上述的代码。在这里,我们利用 JNIEnv 的自变量jEnv 来访问一个
Java 字串。Java 字串采取的是Unicode 格式,所以假若收到这样一个字串,并想把它传给一个非 Unicode 函
数(如printf() ),首先必须用JNI 函数GetStringUTFChars()将其转换成 ASCII 字符。该函数能接收一个
Java 字串,然后把它转换成 UTF…8 字符(用 8 位宽度容纳 ASCII 值,或用 16 位宽度容纳 Unicode;若原始字
串的内容完全由ASCII 构成,那么结果字串也是ASCII)。
GetStringUTFChars 是JNIEnv 间接指向的那个结构里的一个字段,而这个字段又是指向一个函数的指针。为
访问JNI 函数,我们用传统的C 语法来调用一个函数 (通过指针)。利用上述形式可实现对所有JNI 函数的
访问。
A。1。3 传递和使用 Java 对象
在前例中,我们将一个字串传递给固有方法。事实上,亦可将自己创建的 Java 对象传递给固有方法。
在我们的固有方法内部,可访问已收到的那些对象的字段及方法。
为传递对象,声明固有方法时要采用原始的Java 语法。如下例所示,MyJavaClass 有一个 public (公共)字
段,以及一个public 方法。UseObjects 类声明了一个固有方法,用于接收 MyJavaClass 类的一个对象。为
调查固有方法是否能控制自己的自变量,我们设置了自变量的 public 字段,调用固有方法,然后打印出
public 字段的值。
class MyJavaClass {
public void divByTwo() { aValue /= 2; }
public int aValue;
}
public class UseObjects {
public static void main(String '' args) {
UseObjects app = new UseObjects();
MyJavaClass anObj = new MyJavaClass();
anObj。aValue = 2;
app。changeObject(anObj);
System。out。println(〃Java: 〃 + anObj。aValue);
}
private native void
changeObject(MyJavaClass obj);
static {
System。loadLibrary(〃UseObjImpl〃);
}
}
编译好代码,并将。class 文件传递给 javah 后,就可以实现固有方法。在下面这个例子中,一旦取得字段和
方法 ID,就会通过 JNI 函数访问它们。
JNIEXPORT void JNICALL
653
…………………………………………………………Page 655……………………………………………………………
Java_UseObjects_changeObject(
JNIEnv * env; jobject jThis; jobject obj) {
jclass cls;
jfieldID fid;
jmethodID mid;
int value;
cls = env…》GetObjectClass(obj);
fid = env…》GetFieldID(cls;
〃aValue〃; 〃I〃);
mid = env…》GetMethodID(cls;
〃divByTwo〃; 〃()V〃);
value = env…》GetIntField(obj; fid);
printf(〃Native: %dn〃; value);
env…》SetIntField(obj; fid; 6);
env…》CallVoidMethod(obj; mid);
value = env…》GetIntField(obj; fid);
printf(〃Native: %dn〃; value);
}
除第一个自变量外,C++函数会接收一个 jobject,它代表Java 对象引用“固有”的那一面——那个引用是
我们从 Java 代码里传递的。我们简单地读取 aValue,把它打印出来,改变这个值,调用对象的divByTwo()
方法,再将值重新打印一遍。
为访问一个字段或方法,首先必须获取它的标识符。利用适当的JNI 函数,可方便地取得类对象、元素名以
及签名信息。这些函数会返回一个标识符,利用它可访问对应的元素。尽管这一方式显得有些曲折,但我们
的固有方法确实对Java 对象的内部布局一无所知。因此,它必须通过由 JVM 返回的索引访问字段和方法。这
样一来,不同的JVM 就可实现不同的内部对象布局,同时不会对固有方法造成影响。
若运行 Java 程序,就会发现从 Java 那一侧传来的对象是由我们的固有方法处理的。但传递的到底是什么
呢?是指针,还是Java 引用?而且垃圾收集器在固有方法调用期间又在做什么呢?
垃圾收集器会在固有方法执行期间持续运行,但在一次固有方法调用期间,我们的对象可保证不会被当作
“垃圾”收集去。为确保这一点,事先创建了“局部引用”,并在固有方法调用之后立即清除。由于它们的
“生命期”与调用过程息息相关,所以能够保证对象在固有方法调用期间的有效性。
由于这些引用会在每次函数调用的时候创建和破坏,所以不可在static 变量中制作固有方法的局部副本(本
地拷贝)。若希望一个引用在函数存在期间持续有效,就需要一个全局引用。全局引用不是由JVM 创建的,
但通过调用特定的 JNI 函数,程序员可将局部引用扩展为全局引用。创建一个全局引用时,需对引用对象的
“生存时间”负责。全局引用(以及它引用的对象)会一直留在内存里,直到用特定的JNI 函数明确释放了
这个引用。它类似于C 的malloc()和 free()。
A。1。4 JNI 和 Java 异常
利用 JNI,可丢弃、捕捉、打印以及重新丢弃Java 异常,就象在一个 Java 程序里那样。但对程序员来说,
需自行调用专用的JNI 函数,以便对异常进行处理。下面列出用于异常处理的一些JNI 函数:
■Throw():丢弃一个现有的异常对象;在固有方法中用于重新丢弃一个异常。
■ThrowNew():生成一个新的异常对象,并将其丢弃。
■ExceptionOccurred():判断一个异常是否已被丢弃,但尚未清除。
■ExceptionDescribe():打印一个异常和堆栈跟踪信息。
■ExceptionClear():清除一个待决的异常。
■FatalError():造成一个严重错误,不返回。
在所有这些函数中,最不能忽视的就是ExceptionOccurred()和ExceptionClear()。大多数JNI 函数都能产
生异常,而且没有象在 Java 的try 块内的那种语言特性可供利用。所以在每一次 JNI 函数调用之后,都必须
调用ExceptionOccurred(),了解异常是否已被丢弃。若侦测到一个异常,可选择对其加以控制(可能时还
要重新丢弃它)。然而,必须确保异常最终被清除。这可以在自己的函数中用ExceptionClear()来实现;若
异常被重新丢弃,也可能在其他某些函数中进行。但无论如何,这一工作是必不可少的。
654
…………………………………………………………Page 656……………………………………………………………
我们必须保证异常被彻底清除。否则,假若在一个异常待决的情况下调用一个JNI 函数,获得的结果往往是
无法预知的。也有少数几个JNI 函数可在异常时安全调用;当然,它们都是专门的异常控制函数。
A。1。5 JNI 和线程处理
由于Java 是一种多线程语言,几个线程可能同时发出对一个固有方法的调用(若另一个线程发出调用,固有
方法可能在运行期间暂停)。此时,完全要由程序员来保证固有调用在多线程的环境中安全进行。例如,要
防范用一种未进行监视的方法修改共享数据。此时,我们主要有两个选择:将固有方法声明为“同步”,或
在固有方法内部采取其他某些策略,确保数据处理正确地并发进行。
此外,绝对不要通过线程传递 JNIEnv,因为它指向的内部结构是在“每线程”的基础上分配的,而且包含了
只对那些特定的线程才有意义的信息。
A。1。6 使用现成代码
为实现JNI 固有方法,最简单的方法就是在一个Java 类里编写固有方法的原型,编译那个类,再通过 javah
运行。class 文件。但假若我们已有一个大型的、早已存在的代码库,而且想从Java 里调用它们,此时又该
如何是好呢?不可将DLL 中的所有函数更名,使其符合 JNI 命名规则,这种方案是不可行的。最好的方法是
在原来的代码库“外面”写一个封装DLL。Java 代码会调用新 DLL 里的函数,后者再调用原始的DLL 函数。
这个方法并非仅仅是一种解决方案;大多数情况下,我们甚至必须这样做,因为必须面向对象引用调用 JNI
函数,否则无法使用它们。
A。2 微软的解决方案
到本书完稿时为止,微软仍未提供对JNI 的支持,只是用自己的专利方法提供了对非Java 代码调用的支持。
这一支持内建到编译器 Microsoft JVM 以及外部工具中。只有程序用 Microsoft Java 编译器编译,而且只有
在Microsoft Java 虚拟机(JVM)上运行的时候,本节讲述的特性才会有效。若计划在因特网上发行自己的
应用,或者本单位的内联网建立在不同平台的基础上,就可能成为一个严重的问题。
微软与Win32 代码的接口为我们提供了连接 Win32 的三种途径:
(1) J/Direct:方便调用Win32 DLL 函数的一种途径,具有某些限制。
(2) 本原接口(RNI):可调用Win32 DLL 函数,但必须自行解决“垃圾收集”问题。
(3) Java/ 集成:可从 Java 里直接揭示或调用 服务。
后续的小节将分别探讨这三种技术。
写作本书的时候,这些特性均通过了Microsoft SDK for Java 2。0 beta 2 的支持。可从微软公司的Web 站
点下载这个开发平台(要经历一个痛苦的选择过程,他们叫作“Active Setup”)。Java SDK 是一套命令行
工具的集合,但编译引擎可轻易嵌入Developer Studio 环境,以便我们用 Visual J++ 1。1 来编译 Java 1。1
代码。
A。3 J/Direct
J/Direct 是调用 Win32 DLL 函数最简单的方式。它的主要设计目标是与Win32API 打交道,但完全可用它调
用其他任何 API。但是,尽管这一特性非常方便,但它同时也造成了某些限制,且降低了性能(与RNI 相
比)。但J/Direct 也有一些明显的优点。首先,除希望调用的那个DLL 里的代码之外,没有必要再编写额外
的非Java 代码,换言之,我们不需要一个封装器或者代理/存根DLL。其次,函数自变量与标准数据类型之
间实现了自动转换。若必须传递用户自定义的数据类型,那么 J/Direct 可能不按我们的希望工作。第三,就
象下例展示的那样,它非常简单和直接。只需少数几行,这个例子便能调用Win32 API 函数MessageBox(),
它能弹出一个小的模态窗口,并带有一个标题、一条消息、一个可选的图标以及几个按钮。
public class ShowMsgBox {
public static void main(String args'')
throws UnsatisfiedLinkError {
MessageBox(0;
〃Created by the MessageBox() Win32 func〃;
〃Thinking in Java〃; 0);
}
655
…………………………………………………………Page 657……………………………………………………………
/** @dll。import(〃USER32〃) */
private static native int
MessageBox(int hwndOwner; String text;
String title; int fuStyle);
}
令人震惊的是,这里便是我们利用 J/Direct 调用Win32 DLL 函数所需的全部代码。其中的关键是位于示范代
码底部的MessageBox()声明之前的@dll 。import 引导命令。它表面上看是一条注释,但实际并非如此。它的
作用是告诉编译器:引导命令下面的函数是在 USER32 DLL 里实现的,而且应相应地调用。我们要做的全部事
情就是提供与DLL 内实现的函数相符的一个原型,并调用函数。但是毋需在Java 版本里手工键入需要的每一
个Win32 API 函数,一个Microsoft Java 包会帮我们做这件事情(很快就会详细解释)。为了让这个例子正
常工作,函数必须“按名称”由DLL 导出。但是,也可以用@dll。import 引导命令“按顺序”链接。举个例
子来说,我们可指定函数在DLL 里的入口位置。稍后还会具体讲述@dll。import 引导命令的特性。
用非Java 代码进行链接的一个重要问题就是函数参数的自动配置。正如大家看到的那样,MessageBox()的
Java 声明采用了两个字串自变量,但原来的C 方案则采用了两个 char 指针。编译器会帮助我们自动转换标