JNI学习笔记(五)——fields和methods

来源:互联网 发布:mac破解软件论坛 编辑:程序博客网 时间:2024/06/05 10:00

之前的学习,知道了JNI可以让native代码访问基础类型和引用类型,本章节,我们要学习如果访问一个对象的字段(其实就是对象中的变量)和方法。此外,还将学习如何在native代码调用java编程语言实现的方法——这对回调函数,尤其有用。



访问字段


java编程语言,支持两种字段:实例字段和static字段,(可以这么理解:实例变量和static变量)。

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.accessField方法的native代码实现:
JNIEXPORT void JNICALLJava_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"


访问一个实例字段的步骤


为了进入一个实例字段,native方法遵循两个过程:第一,调用GetFieldID,由相关的类、字段名字和字段描述符,来获得字段field的ID:
fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String");

上示例子中的代码,相关的类cls是通过在相关对象obj上调用GetObjectClass获得的。一旦获得了字段ID之后,就可以把相关对象和该字段ID传给合适的实例字段访问函数。由于字符串和数组是特殊类型的对象,我们使用GetObjectField来访问例子中的String实例变量:
jstr = (*env)->GetObjectField(env, obj, fid);
在GetObjectField和SetObjectField 函数之外,JNI还提供了访问基础类型的实例字段的方法,例如GetIntField,SetIntField,GetFloatField,SetFloatField等等。



字段描述符(重头戏上马了)


JNI使用一种叫做”JNI字段描述“的C字符串来表示java编程语言中的字段的类型。如之前出现的”Ljava/lang/String;“来表示java编程语言中的String类型,”I“表示int,”F“表示float,”D“表示double,”Z“表示boolean等等。

一个引用类型的描述符,例如java.lang.String,由L字母开头,并且以分号结束,在类的全名”java.lang.String“中的”.“被”/“替换。所以java.lang.String被表示为:”Ljava/lang/String;"

数组的描述符包含”[“字符,紧随其后的是数组的类型,例如java语言中的int[ ]数组,在JNI中这样表示:”[I“。

可以用javap工具来从类文件中生成字段描述符,通常情况下,javap输出一个给定类中的方法和字段。而如果加上-s选项,javap则输出JNI描述符:
javap -s InstanceFieldAccess
它输出了以下结果:
...
s Ljava/lang/String;
...

基础类型的JNI描述符

JNI字段描述符java编程语言ZbooleanBbyteCcharSshortIintJlongFfloatDdouble

访问静态字段


访问一个静态字段和访问实例字段是相似的:
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");    }}

与访问实例字段不同的是,访问静态字段时,使用GetStaticFieldID
JNIEXPORT void JNICALLJava_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 = 100In Java:  StaticFieldAccess.si = 200


从上所示代码,我们可以看出访问实例字段和访问静态字段,有两处不同:
1)之前已经提到的,用GetStaticFieldID替代访问实例字段中用到GetFieldID。
2)在得到静态字段ID之后,使用合适的静态字段方法。


调用方法


在java语言中有多种方法,实例方法,静态方法,构造方法,等等。JNI支持一组允许你在native代码中执行回调的函数。
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");    }}

native方法的实现:
JNIEXPORT void JNICALLJava_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 CIn Java


调用实例方法


首先要取得方法的ID,例子中调用了GetMethodID来获取MethodID。该函数,在给定类中寻找该方法。寻找的标准基于方法名以及方法的类型描述符。如果方法不存在,怎函数返回NULL。并且在java语言中调用该naitive方法的调用者处抛出异常NoSuchMethodError。

然后,调用CallVoidMethod(该方法调用了一个返回类型是void的实例方法)。在此,给该方法传入对象,方法ID,以及实际参数。

在CallVoidMethod之外,JNI同样也支持调用其他返回类型的函数,如CallIntMethod。同样也可以使用CallVoidMethod来调用返回对象的方法。(如返回值是字符串或者数组)



格式化方法描述符


和字段一样,native方法需要描述符来告诉JNI native代码和java语言中对应的方法。(或者更加合适的叫法叫做方法签名)。一个方法的描述符,由参数类型、方法返回值类型组成。参数类型在前,并且由一对括号包围,方法返回值类型紧随其后,在多个参数之间没有分隔符。例如一个返回值为void型,并且拥有一个int型参数的方法,可以由“(I)V”来表示。而"()D"则表示一个返回值是double型的,没有参数的方法。
1)方法的描述符,可能还包含类描述符(类描述符,我们将在后面学到),java中的代码是:
private native  String getLine(String);
则,其方法描述符是:
“(Ljava.lang.String;)Ljava.lang.String;”
2)数组的描述是“[”开头的,后面更数组元素的类型的描述,所以
public static void main(String[ ] args);
的描述符是:
"(Ljava.lang.String;)V"

下表提供了一个关于如何格式话方法描述符的完整的描述:
方法描述符java语言类型"()Ljava/lang/String"String f();"(ILjava/lang/Class)J"long f(int i, Class c);"([B)v"void f(byte[ ] bytes);

调用静态方法

这是一个和访问静态字段相对应的章节,自然与之相似,在调用静态方法时,和调用实例方法,也有两点不同:
1)调用静态方法时,取GetStaticMethodID而代实例方法中的GetMethodID。
2)改调用JNI函数CallVoidMethod为调用CallStaticVoidMethod,同样JNI也为静态方法提供了,CallStatic<Type>Method系列方法。

在java语言中,可以这样调用Class cls的静态方法f:cls.f或者obj.f。然而在JNI中,在调用静态方法时,必须指定引用类。再看一个例子:
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");    }}

native代码中的实现:
JNIEXPORT void JNICALLJava_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);}

输出结果:
In CIn Java


调用一个超类的实例方法


之前我们了解了调用一个类的实例方法和静态方法。这里介绍如何调用一个已经在子类中被覆盖了的超类的方法。JNI提供了一组CallNonvitural<Type>Method函数来实现这个目的。为了调用在超类中的实例方法,需要遵循以下步骤:
1)用GetMethodID来从该超类的一个引用中获取方法的ID。
2)给nonvirtual族的合适的JNI函数(例如CallNonvirtualVoidMethod,CallNonvirtualBooleanMethod等)传参数:对象、超类、方法ID以及方法的参数。

这种调用超类中的实例方法的情况很少见。这里介绍的方法和在java语言中调用一个被覆盖的超类的方法的情形比较相似(在java语言中,使用构造函数:super.f())。

CallNonvirtualVoidMethod同样也能调用构造函数。


调用构造函数


在JNI中,构造函数可以和其他实例方法一样,以类似的步骤被调用。为了获得一个构造函数的方法ID,将"<init>"作为方法名,并且在方法描述符中,用”V“作为方法的返回类型。这之后,就可以调用构造函数,并且传递方法ID到JNI函数(例如NewObject)。以下代码,用java.lang.String构造函数,实现一个等同JNI函数NewString功能的函数。
jstringMyNewString(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;}

这段代码曾经在我的《JNI学习笔记(一)》中出现过。它从一个以Unicode编码方式存储在C缓冲区中的字符串,构造为java.lang.String的一个对象,和NewString功能等效。
1)首先,FindClass返回一个java.lang.String类的引用。
2)然后,GetMethodID返回java.lang.String的构造函数String(char[ ] chars)的方法ID。
3)接着,调用NewCharArray来分配一个字符数组,用来保存所有的字符串的元素。
4)再后来,JNI函数NewObject函数,调用了由方法ID指定的构造函数,来构造对象。NewObject的参数:类的引用,方法ID,以及该构造函数所需要的参数。

DeleteLocalRef函数用来允许VM释放被本地引用elemArr和stringClass所使用的资源。在下一章节我们将详细学习DeleteLocalRef。

既然我们能够实现一个等效的函数,那为什么JNI还要提供一个内建的函数(例如NewString)?这是因为,内建字符串函数远远比在native code中调用java.lang.String API更高效。字符串是一个被高频使用的对象类型,一个像这样的,值得JNI作出特殊支持。

同样也有可能使用CallNonvirtualVoidMetho方法来调用构造函数,在这个例子里,native代码必须首先通过AllocObject函数来创建一个未初始化的对象。这样:
result = (*env)->NewObject(env, stringClass, cid, elemArr);
可以被AllocObject和CallNonvirtualVoidMetho方法替代:
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创建了一个未初始化的对象,但是使用时必须要小心,所以对于每个对象,构造函数最多只能被调用一次。不可以在native代码对同一个对象调用多次构造函数。

虽然有时候,可能会发现,先创建一个对象,然后再之后的某个时间调用构造函数,非常有用。但是更多时候,应该使用NewObject来避免和减少因为使用AllocObject、CallNonvirtualVoidMethod对而带来更容易错误的几率。


抓住字段和方法的ID


为了获取字段和方法的ID,需要基于字段和方法的名字、和描述符来进行符号查找。符号查找是相对昂贵的,本节,介绍一种可以降低这种费用的技术。

这个想法是:计算字段和方法ID,并且为后面重复使用它们而缓存它们。有两种方式可以缓存字段、方法ID,取决于缓存是否执行在使用字段和方法ID的点上,或者是在定义自动和方法的类的静态初始化器中。

在使用是捕获


字段和方法ID可以在native代码访问字段值或者执行方法回调的时候被捕获。下面的Java_InstaceFieldAccess_accessField函数的实现中,缓存字段ID到一个静态变量中,这样就不需要在每次调用的InstanceFieldAccess.accessField时候都去重新计算。
JNIEXPORT void JNICALLJava_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);}

同样,我们可以缓存java.lang.String构造函数的方法ID:
jstringMyNewString(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;}


在类的初始化时捕获


在使用时捕获字段和方法的ID,我们必须检查ID是否已经被缓存了。可是它同时会导致重复的缓存和检查。如果有多个native方法需要访问同一个字段,那么他们都需要检查、计算并且缓存对应的字段ID。

在更多情形下,在应用程序有机会调用native方法之前,就初始化字段和方法ID,将更加便利。VM一直都在调用一个类的任何方法之前,先执行该类的static初始化器。所以,在初始化器中执行计算、缓存字段和方法ID是一个合适的地方。

例如,为了缓存InstanceMethodCall.callback的方法ID,我们引入一个新的native方法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();    }}

initIDs的实现:
jmethodID MID_InstanceMethodCall_callback;JNIEXPORT void JNICALLJava_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls){    MID_InstanceMethodCall_callback =        (*env)->GetMethodID(env, cls, "callback", "()V");}

虚拟机VM运行静态初始化器,并且在执行任何其他方法(例如nativeMethod、main)之前,调用initIDs方法,在InstaceMethodCall.nativeMthod方法ID被保存到全局变量以后,它的实现中,就不需要在进行符号检查了:
JNIEXPORT void JNICALLJava_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj{    printf("In C\n");    (*env)->CallVoidMethod(env, obj,                           MID_InstanceMethodCall_callback);}


比较两种缓存ID的方法


在使用时捕获、缓存IDS的方法,如果JNI开发人员不能掌握定义字段或者方法的类的源代码时,是一个合理的解决方案。例如,在MyNewString例子中,我们不能向java.lang.String类注入一个自定义的initIDs方法。

和在类的静态初始化器中缓存相比,在使用时缓存有一些缺点:
1)在使用时缓存,需要对同一个字段和方法ID进行重复的检查和初始化。
2)直到协作类时,方法和字段ID一直都是有效的。如果在使用时缓存,必须保证定义它们的的类在native代码还依赖缓存的ID值期间,不会被卸载和重新载入。另外一方面,如果是在静态初始化器中完成缓存ID,那么这些缓存的ID在类被卸载和重新载入时,会被自动地重新计算。

因此在可能的情况下,最好是在类的静态初始化器中缓存自动和方法的ID。


JNI字段和方法操作的性能


在学习了如何缓存字段和方法ID来提高性能之后,可能会想知道:使用JNI访问字段和调用方法的性能特点是什么?在native代码中执行一个回调的费用和在java中调用一个native 方法的费用,以及一个普通的方法调用的费用的比较,如何?

这些问题的答案,无疑需要依赖VM实现JNI的效率。所以并不能够给出一个确切的性能特性。所以这里,我们只分析native方法固有的费用,调用JNI自动和方法操作的费用,以及提供一个总体上的性能指引。

比较java/native调用的的开销和java/java调用的开销,java/native调用可能比java/java调用慢的原因:
1)native方法比在java VM中实现的java/java调用更有可能遵循一个不同的调用转换。这样一来,VM必须在进入native方法入口前,执行额外的操作,来建立和设置堆栈帧。
2)内联方法调用,是虚拟机常见的方式。内联java/native调用比内联java/java调用困难很多。

我们估计,一个典型的VM执行一个java/native调用比执行一个java/java调用可能要慢上2~3倍。因为java/java调用,只需要很少的几个周期,因此而增加的开销激活可以忽略不计(除非nativ方法只是执行琐碎的操作)。生成一个java/native调用的性能和java/java调用的性能接近或者相等的虚拟机实现,是可能的(这类VM实现,可能会采用JNI调用约定来作为内部JAVA/JAVA调用的约定)。

一个native/java回调的性能特点,在技术上和java/native调用是相近的。从理论上讲,native/JAVA回调的性能可能也在java/java调用的2~3倍之间。然而,实际上,native/java回调是比较少见的。VM实现,一般不会优化回调的性能。所以一个native/java回调的开销可能比java/java调用的开销高出10之多。

使用JNI访问字段的开销主要花费在通过JNIEnv调用上。native代码必须执行一个C函数调用,来解引用对象,而不是直接在解引用对象上。这样的函数调用是有必要的,因为它使由VM实现管理的内部对象和native代码分开来。JNI字段访问的开销,通常可以忽略不计的,因为一个函数调用只需要很少的周期。