之前的学习,知道了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 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"


访问一个实例字段的步骤


为了进入一个实例字段,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编程语言
Z boolean
B byte
C char
S short
I int
J long
F float
D double


访问静态字段


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


调用实例方法


首先要取得方法的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 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);
}

输出结果:
In C
In 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功能的函数。
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;
}

这段代码曾经在我的《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 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);
}

同样,我们可以缓存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;
}


在类的初始化时捕获


在使用时捕获字段和方法的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 JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls)
{
MID_InstanceMethodCall_callback =
(*env)->GetMethodID(env, cls, "callback", "()V");
}

虚拟机VM运行静态初始化器,并且在执行任何其他方法(例如nativeMethod、main)之前,调用initIDs方法,在InstaceMethodCall.nativeMthod方法ID被保存到全局变量以后,它的实现中,就不需要在进行符号检查了:
JNIEXPORT void JNICALL
Java_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字段访问的开销,通常可以忽略不计的,因为一个函数调用只需要很少的周期。










更多相关文章

  1. 使用字符串参数调用AndroidJni静态方法。
  2. java代码获知该方法被哪个类、哪个方法、在哪一行调用
  3. Play 2.0生成隐藏字段而不使用div包装器
  4. spring框架中一个跟String的trim方法一样的方法
  5. 当只使用get()和set()方法时,用原始类型替换AtomicBoolean?
  6. java中循环遍历删除List和Set集合中元素的方法
  7. 80端口占用异常解决方法java.net.BindException: Address alread
  8. java写入文件的几种方法小结
  9. OOP面向对象编程(一)-------方法的重载

随机推荐

  1. 虚函数和纯虚函数的区别是什么?
  2. c语言的输入输出语句有哪些?
  3. windows.h有哪些函数
  4. c语言中字符常量是什么?
  5. C语言中的指针有什么作用
  6. c语言程序中的基本功能模块为什么?
  7. C语言的三种基本程序结构是什么
  8. c语言源文件经过编译后生成文件的后缀是
  9. c语言是一种什么的程序设计语言?
  10. c语言指针用法有哪些