本文共 16420 字,大约阅读时间需要 54 分钟。
原文链接:
第四章 成员和方法(CHAPTER 4 Fields and Methods)
现在你知道"JNI"怎样让本地代码访问基本类型和类型引用例如"strings"和"arrays",下一步将是学习怎样和在任意对象(objects)中的成员(fields)和方法(methods)来交互。除了访问成员域,还包含从本地代码中调用用Java编程语言实现的方法,一般被认为从本地代码中执行回调(performing callbacks)。
我们将通过介绍支持成员域访问和方法回调的JNI函数开始。这章节的后面,我们将讨论怎样通过使用一个简单但有效的缓冲技术(simple buf effective caching technique)来更有效的做如此操作。在最后章节,我们将讨论调用本地方法的性能特点,和从本地方法中访问成员域和调用方法的性能特点。
4.1 访问成员域(Accessing Fields)
Java编程语言支持两种类型的成员域。一个类的每个实例有这个类型的实例成员域(the instance fields of the class)的自己副本,还有一个类的所有实例共享这个类的静态成员域(static fields of the class)。
JNI提供函数,本地代码能使用用它来得到和设置在对象(objects)中的实例成员域和在类中的静态成员域。让我们首先看一个例子程序,这程序说明怎样从一个本地方法的实现中访问实例成员域。
class InstanceFieldAccess{
"InstanceFieldAccess"类定义一个实例成员域"s"。"main"方法创建了一个对象(object),设置实例成员域,然后调用本地方法"InstaceFieldAccess.accessField"。正如我们将要很快看到的,本地方法答应出实例成员域存在的值,然后设置成员域为一个新的值。在本地方法返回后,程序又打印这个成员域值,证明这个成员域值研究真正地改变了。
"InstanceFieldAccess"本地方法的实现。
JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {
运行带有"InstanceFieldAccess"本地库的"InstanceFieldAccess"类产生下面的输出:
In C:
4.1.1 访问一个实例成员域的成果(Procedure for Accessing an Instance Field)
为了访问一个实例域,本地方法分两步处理。首先,他调用"GetFieldID"从类引用,成员域的名字,和成员域描述符,来得到成员域(field)ID: fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;") ;
这个例子代码通过调用"GetObjectClass"在实例引用对象(obj)上得到类的引用,"obj"被作为第二个参数传递给本地方法实现。
一旦你已经得到成员域"ID",你能够传递对象引用(object reference)和成员域ID(Field ID)给相应的实例域访问函数:
jstr = (*env)->GetObjectField(env, obj, fid) ;
因为"strings"和"arrays"是个特别的对象类型,我们使用"GetObjectField"来访问的这个实例域是一个字符串(string)。除了"Get/SetObjectField","JNI"也支持其他函数例如"GetIntField"和"SetFloatField"来访问基本类型的实例成员域。
4.1.2 成员域的描述符(Field Descriptors)
你可能已经注意到在前面的部分中,我们使用一个特殊的编码"C"字符串"Ljava/lang/String"来表示一个成员域在"Java"编程语言中的类型。这些"C strings"被称为"JNI"成语域描述符(JNI field descriptors)。
成员域声明的类型决定了这个"string"的内容。例如,你用"I"来代表一个"int"成员域,用"F"来代表一个"float"域,用"D"来代表一个"double"成员域,用"Z"来代表一个"boolean"成员域,等等(and so on)。
一个类型引用的描述符,例如"java.lang.String",带有字符"L"开始,它被JNI类描述符跟着,同时以一个分号(semicolon)结束。在完全合格的类名字中的"."分割符被变成在"JNI"类描述符中的"/"。因此。你为类型为"java.lang.String"的成员域建立了成员域描述符,如下:
"Ljava/lang/String;"
对于数组类型的描述符包含"["字符,被数组的组成类型的描述符跟着。例如,"[I"是"int[]"成员域类型的描述符。12.3.3部分包含成员域描述符的详细信息,同时在Java程序语言中它们匹配的类型。
你能使用"javap"工具(随带"JDK"或"Java 2 SDK releases"发布)来从类的文件产生成员域描述。一般地,"javap"打印出在给出的类中的方法和成员类型。如果你指定了"-s"可选项(和为导出私有成员的"-p"可选项),"javap"打印JNI描述发来替代:
javap -s -p InstanceFieldAccess
这给你输出包含成员域"s"的"JNI"描述符:
... s Ljava/lang/String ...
使用"javap"工具帮助消除错误,错误可能发生来自手工的"JNI"描述符字符串。
4.1.3 访问静态成员域(Accessing Static Fields)
访问静态成员域和访问实例成员域是相似的。让我们看一个"InstanceFieldAccess"例子的小的变化: class StaticFieldAccess{
"StaticFieldAccess"类包含一个静态整数成员域"si"。"StaticFieldAccess.main"方法创建了这个对象(object),初始化了静态成员域,然后调用了本地方法"StaticFieldAccess.accessField"。很快我们将看到,本地方法打印出静态成员域的存在的值,然后设置这个成语域为一个新值。为了验证成员域被真正地改变,在本地方法返回后,程序又打印了静态成员域的值。
"StaticFieldAccess.accessField"本地方法的实现。
JNIEXPORT void JNICALL Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj) {
运行带有本地库的程序产生下面的输出:
In C:
你怎样访问一个静态成员域和怎样访问一个实例成员域之间有两个不同:
1.你调用"GetStaticFieldID"来得到静态成员域,相对应"GetFieldID"来得到实例成员域。"GetStaticFieldID"和"GetFieldID"有一样的返回类型"jfieldID"。 2.一旦你已经得到静态成员域"ID",你传递了类的引用,相对于一个对象引用, 来给适当静态成员域访问函数。
4.2 调用方法(Calling Methods) 在"Java"编程语言中有几种类型的方法。实例方法必须在一个类的具体实例上被调用,然而静态方法可以不依赖任何实例被调用。我们将推迟构建(constructors)的讨论到下一部分中。
"JNI"支持一整套函数来允许你执行来自本地代码的回调。这个下面程序例子包含一个本地方法,反过来调用了用Java编程语言实现的一个实例方法。
class InstanceMethodCall{
这儿是本地方法的实现:
JNIEXPORT void JNICALL Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {
运行上面程序产生下面的输出:
In C In Java
4.2.1 调用实例方法(Calling Instance Methods)
"Java_InstanceMethodCall_nativeMethod"实现说明了两步来请求调用一个实例方法: .本地方法首先调用"JNI"函数"GetMethodID"。"GetMethodID"为在被给的类中方法执行一个查询。查询是依靠名字和方法的类型描述符。如果方法没有,"GetMethodID"返回"NULL"。在这点,从本地代码的一个立马返回产生一个"NoSuchMethodError"异常,在调用"InstanceMethodCall.nativeMethod"的代码中被抛出。 .然后本地代码调用"CallVoidMethod"。"CallVoidMethod"调用一个实例方法,它返回一个void类型。你传递一个对象(object),方法ID和实际参数(虽然在上面的例子中没有)给"CallVoidMethod"。
除了"CallVoidMethod"函数,"JNI"也支持带有其他类型返回的方法调用函数。例如,如果你回调方法返回一个"int"类型的值,你的本地方法应该使用"CallIntMethod"。类似地,你能使用"CallObjectMethod"来调用方法,放回一个对象(objects),它包含"java.lang.String"实例和数组。
你最好能使用"Call<Type)Method"系列函数来调用接口方法。你必须从接口类型来得到方法"ID"。例如,下面的代码片段调用"Runnable.run"方法在一"java.lang.Thread"实例上:
jobject thd = ... ; jmethodID mid ; jclass runnableIntf = (*env)->FindClass(env, "java/lang/Runnable") ; if ( runnableIntf == NULL ){
我们在3.3.5中了解了FindClass函数返回一个名字类的引用。这儿总是得到一个命名的接口的应用。
4.2.2 构造方法描述符
"JNI"是用的构造描述符字串来表示方法类型,和怎样表示类型成员域用一样方法。一个方法描述符联合一个方法的参数类型和返回类型。首先参数类型出现在,同时被一堆括号(parentheses)包围。参数类型被有序的列表,它们按照在方法声明中顺序出现。在多个参数类型之间没有分割符。如果一个方法没有参数,用一对空的括号来表示。在为参数类型的右闭括号后,立即放置方法的放回类型。
例如,"(I)V"表示带有一个"int"类型参数和返回值为"void"的一个方法。"()D"表示一个没有参数和返回"double"类型的一个方法。不要让"C"函数原型例如"int f(void)"误导你认为"(V)I"一个有效方法描述符。要使用"()I"来替代。
方法描述符可以包含类描述符.例如,方法:
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"方法描述符。例如,通过运行:
javap -s -p InstanceMethodCall
你得到下面输出:
... private callback ()V public static main([Ljava/lang/String;)V private native nativeMethod ()V ...
"-s"标记通知"javap"来输出"JNI"描述符字串,而不是像它们在Java编程语言中出现的类型。"-p"标记引起"javap"来包含这类的私有成员的信息在它的输出中。
4.2.3 调用静态方法(Calling Static Methods)
前面的例子示范本地代码怎样调用一个实例方法。类似地,你能从本地代码执行静态方法的回调,通过下面步骤: .用"GetStaticMethodID"来获得方法"ID",对应于"GetMethodID"。 .传递类,方法"ID"和参数给系列静态方法调用函数的一个:"CallStaticVoidMethod","CallStaticBooleanMethod",等等(and so on)。
在允许你调用的静态方法的函数和允许你调用实例方法的函数之间有一个关键的不同点。前者(former)调用一个类的引用(class reference)作为第二个参数,然而后者(latter)使用一个对象应用(object reference)作为第二个参数。例如,你传递类的引用到"CallStaticVoidMethod",但是传递一个对象应用到"CallVoidMethod"。
在Java编程语言层,你能够使用两种可选语法,调用在类"Cls"中的一个静态方法"f":"Cls.f"或者"obj.f",这儿"obj"是一个"Cls"的实例。(然而,后者是被推荐的编程格式)在"JNI"中,你必须总是指明类的引用,在从本地代码中调用静态方法时。
让我么看一个例子,它从本地代码中使用了一个静态方法的回调。它是前面(earlier)"InstanceMethodCall"例子的一个小变化:
class StaticMethodCall{
这儿是本地方法的实现:
JNIEXPORT void JNICALL Java_StaticMehtodCall_nativeMethod(JNIEnv *env, jobject obj) {
确信你传递"cls"(用粗体高亮),相对于"obj",来给"CallStaticVoidMethod"。运行上面的程序产生下面期待输出:
In C In Java
4.2.4 调用一个超类的实例方法(Calling Instance Methods of a Superclass)
你能调用实例方法,它被定义在一个超类中,但是在这个类中已经被重载了,这对象属于这类。"JNI"提供一套"CallNonvirtual<Type>Method"函数来为这个目的。为调用被定义在一个超类中的一个实例方法,你如下做: .使用"GetMethodID"来从超类的引用来得到方法"ID"。 .传递对象(object),超类(superclass),方法"ID"和参数给系列非虚拟调用函数(nonvirtual invocation functions)的一个,例如"CallNovirtualVoidMethod", "CallNovirtualBooleanMeth
相对地很少你将需要调用一个超类的实例方法。这个工具和调用一个重载超类方法类似,说"f"吧,在Java编程语言总用下面结构:
super.f();
"CallNovirtualVoidMethod"也能被用来调用构造器(constructors),像下一部分将要说明的。
4.3 调用构造器(Invoking Constructors)
在"JNI"中,按照下面几步,构造器可以被调用,类似于为调用实例方法。为获得一个构造器的方法"ID",传递"<init>"作为方法名字和在方法描述符中"V"作为返回类型。然后,你能通过传递方法ID给"JNI"函数来调用构造器,例如NewObject。接下来的代码实现了和"JNI"函数"NewString"一样的功能,他构建一个java.lang.String的对象,从来自一个"C buffer"中存储的Unicode字符串(Unicode characters): jstring MyNewString(JNIEnv *env, jchar *Chars, jint len) {
这个函数是相当复杂需要细致的解释。首先,"FindClass"返回一个"java.lang.String"类的引用。下一步,"GetMethodID"为字符串构造器(string constuctor)返回方法"ID",String(char[] chars)。然后,我们调用"NewCharArray"来分配一个字符数组来得到所有字符串元素。"JNI"函数"NewObject"函数使用被构建类型引用,构建器的方法"ID"和需要传递给构建起的参数来做参数。
"DeleteLocalRef"调用允许虚拟机来释放被"elemArr"和"stringClass"局部引用来使用的资源。5.2.1部分将提供一个详细描述关于你什么时候和为什么应该调用"DeleteLocalRef"。
"Strings"是个对象。这个例子进一步指明这点。然而,例子也提出一个问题。给出我们能使用其他"JNI"函数实现一样的功能,为什么JNI提供内建函数例如NewString?原因是内建字符串函数(built-in string functions)更高效于从自本地代码中调用"java.lang.String"API。"String"是最常被使用的对象类型(type of objects),在"JNI"中值得特别支持。
也可能使用"CallNonvirtualVoidMethod
可以通过一个"AllocObject"调用跟着通过一个"CallNovirtualvoidMethod"调用来替代它:
result = (*env)->AllocObject(env, stringClass) ; if(result){
"AllocObject"创建一个没初始化的对象,同时不需小心被使用,所以一个构造器在每个对象撒谎那个最多调用一次。本地代码不应该多次地调用一个构造器在同一个对象上。
有时候,你可以发现首先分配一个没有初始化的对象和而后某时调用构造器是有用的。然而,大多数情况中你应该使用"NewObject",同时避免更容易产生错误的AllocObject/CallNovirtualVoidMethod对。
4.4 缓冲成员域和方法ID(Caching Field and Method IDs)
得到成员域和方法"ID"需要基于名字和成员域或方法的描述符的符号的查找。符号的查找是相对费时的。在这部分,我们介绍一中技术,被用来减少这个开销。
这个方法是为计算成员域和方法"ID",同时缓冲它们为以后再次使用。对于缓冲成员域和方法"ID"这儿有两种方法,依赖于是否在成员域或方法ID的使用点时执行缓冲,或在定义成员域或方法的类的静态初始化中来执行缓冲。
4.4.1 使用时缓冲(Caching at the Point of Use) 成员域和方法ID可以被缓冲,在本地代码访问成员域值或执行方法回调的地方。"Java_InstanceFieldAccess_accessField"函数的下面实现,缓冲成员域的ID到静态变量中,所以在"InstanceFieldAccess.accessField"方法的每次调用时不需要被再次计算。
JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv *env,jobject obj) {
高亮的静态变量"fid_s"存储前一次计算的成员域ID为"InstanceFieldAccess.s"。静态变量被初始化为"NULL"。当"InstanceFieldAccess.accessField"方法第一次调用时,它计算成员域的ID同时为以后的使缓冲在静态变量中。
你可能注意到在上面代码中一个明显的判断条件。多个线程可以在相同的时间调用"InstanceFieldAccess.accessField"方法,同时并发地计算一样的成员域ID.一个线程可以通过另一个线程重写静态变量"fid_s".幸运地,虽然这个判断条件在躲线程中导致重复操作,但没有坏处。这个被多线程计算为了在同一个类中的同样的成员域的成员域ID将一定是同样的。
按照同样的注意,我们也可以为在较早"MyNewString"例子中的"java.lang.String"的构造器,缓冲方法"ID":
jstring MyNewString(JNIEnv *env, jchar *char, jint len) {
我们为"java.lang.String"的构造器来计算方法ID,当"MyNewString"第一次被调用时。高亮的静态变量"cid"缓冲了结果。
4.4.2在定义类的初始化中缓冲(Caching in the Defining Class's Initializer) 当我们在使用的地方缓冲一个成员域或一个方法"ID",我们必须引入一个检查来侦测是不是"ID"已经有缓冲了。这个方法不但在加速路上(on the fast path)当"ID"已经缓冲的时候,导致一个小的性能缺陷,而且也可能导致多次缓冲和检查的。例如,入股多个本地方法都请求访问同一成员域,然后它们都需要一个检查来计算和缓冲相应的成员域"ID"。
在许多情况中,在应用程序有机会调用本地方法前,通过一个本地方法的请求来更方便的初始化成员域和方法ID。在调用在这个类的任何方法前,虚拟机总是执行这个类的静态初始化。因此对于计算和缓冲成员域或方法ID的一个适合的地方是在定义了成员域或方法的类的静态初始化中。
例如,为缓冲"InstanceMethodCall.callback"的方法ID,我们引入一个全新本地方法"initIDs",在"InstanceMethodCall"类的静态初始化中调用:
class InstanceMethodCall{
对比在4.2部分中的原始代码,上面的程序包含两个额外的行(用粗体字的高亮)。"initIDs"的实现仅仅地计算和缓冲了"InstanceMethodCall.callback"的方法ID:
jmethodID MID_InstanceMethodCall_callback ;JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls) {
这虚拟机运行静态初始化,同时在执行在"InstanceMethodCall"类中的任何其他方法(例如"nativeMethod"或"main"前,轮到调用"initIDs"方法。在全局变量中缓冲了方法"ID"时,"InstanceMethodCall.nativeMethod"的本地方法的实现不再需要执行一个符号查找了:
JNIEXPORT void JNICALL Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {
4.4.3 在两种缓冲ID的方法间的比较(Comparison between the Two Approaches to Caching IDs)
如果"JNI"编程者不可以控制定义成员域或方法的类的源代码,在使用时缓冲"IDs"是个有效合理的解决方法。例如,在"MyNewString"例子中,我们不能注入一个客户的"initIDs"本地方法到"java.lang.String"类中,为了预计算和缓冲"java.lang.String"的构造器的方法"ID"。
当和在定义类的静态初始化中缓冲比较时,在使用时缓冲有许多缺点。
.像前面解释,在使用时的缓冲在执行快速路径中时需要一个检查,同时也可能需要多个相同成员域或方法ID的初始化和检查。 .方法和成员域"IDs"是有效的直到类被载出。如果你在使用时缓冲了成员域和方法"IDs",在你本地代码依赖缓冲ID的值时,你必须确保定义的类不被载出和重载入。(下一章将显示你怎样通过创建那个使用"JNI"的类的一个引用,能保持一个不被载出。)另一方面,如果定义的类的静态初始化中做了缓冲,缓冲IDs将自动地被计算,当类被载出和后面又载入时。
因此,如果可行的话,最好是在它们定义类的静态初始化中来缓冲成员域和方法"IDs"。
4.5 JNI成员域和方法操作的性能(Performance of JNI Field and Method Operations)
学习怎样缓冲成员域和方啊ID来增强性能后,你可能惊奇:使用"JNI"访问成员域和调用方法的性能特点是什么啊?从本地代码(一个本地/Java回调)执行一个回调的花费和调用一个本地(一个Java/本地调用)的花费比较,还有和调用一般方法(一个Java/Java的调用)的花费比较怎养?
让我们通过比较Java/native调用和Java/Java调用的花费开始。Java/native调用时潜在地比Java/Java调用慢,因为下面的原因:
.在虚拟机器实现中,本地方法更可能比Java/Java调用使用一个不同的调用协定。做为一个结果,虚拟机器必须执行额外的操作来建立参数和构建栈(build argument and set up the stack frame),在进入一个本地方法入口点前。 .它对于对于虚拟机器一般是内联的方法调用。内联Java/native调用比内联Java/Java调用困难许多。
我们估计,一个典型的虚拟机可能执行一个Java/native调用大约慢两到三倍于执行一个Java/Java调用。因为一个Java/Java调用只需要几个周期,增加的开销可以忽略不计,除非本地方法执行简单操作。也可能建立的虚拟机的实现,Java/native调用的性能接近或等于Java/Java调用的性能。(例如,这样的虚拟机的实现可能采用了"JNI"调用协议像内部Java/Java调用协议。)
一个native/Java回调的性能特征是技术上类似于一个Java/native调用。因此,native/Java回调的开销也可能是大约两到三被于Java/Java调用。然而,事实上,native/Java回调相对少见。虚拟机的实现通常不优化回调的性能。在写的许多产品的时候,虚拟机的实现就是如此,一个native/Java回调的开销比一个Java/Java调用要高十倍一样多。
用"JNI"来访问成员域的开销依赖于通过"JNIEnv."调用的开销,而不是直接地取对象(object)的值,本地方法必须执行一个C函数调用来取对象的值。这个函数调用时必须的,因为它隔离本地代码于被虚拟机实现来维护的内部对象表示。"JNI"成员域访问的开销是典型的可以忽略不计的,因为一个函数调用只有几个周期。
转载地址:http://wsmbi.baihongyu.com/