JNI技术解析第四章 访问类成员变量和成员方法

由于篇幅较长,因此可能要花好几天才能写完这一章。下面这一部分是05/14/2015完成的。

这一章介绍访问任意对象的成员变量和方法(调用Java中的方法就是从原生代码回调Java方法)。在后面的章节中也会介绍如何有效快速的进行访问。

4.1 访问类成员变量

Java的类含有两种成员变量:静态成员变量和非静态成员变量。

我们首先介绍如何读取/修改非静态成员变量。

4.1.1 访问非静态成员变量

下面看一个简单的访问类成员变量的例子。

Java代码:


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"); 
    }
}
原生代码中定义了私有成员变量为String类型,在main方法中首先初始化其值为"abc",通过原生代码读取和修改其值后,在Java中打印出修改后的成员变量的值。 下面是C代码:
#include <jni.h>
#include "InstanceFieldAccess.h"

JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)
{
    //获取obj指定的类
    jclass clazz = (*env)->GetObjectClass(env, obj);
    if(NULL == clazz)
        return;
    //通过该类定义,找到该类的名称为s的成员变量,其类型签名为"Ljava/lang/String;",即String类型
    jfieldID sid = (*env)->GetFieldID(env, clazz, "s", "Ljava/lang/String;");
    if(NULL == sid)
        return;
    //获得fieldID后,通过obj读取该成员变量的值,为jstring类型。
    jstring s = (*env)->GetObjectField(env, obj, sid);
    if(NULL == s)
        return;
    //这里就很熟悉了,将引用类型转换为C/C++相应类型。
    const char *ss = (*env)->GetStringUTFChars(env, s, NULL);
    if(NULL == ss)
        return;
    //打印
    printf("In C:n c.s = "%s"n", ss);
    //打印完成后,不再使用const char *ss,就释放掉。
    (*env)->ReleaseStringUTFChars(env, s, ss);
    //用新的值生成一个新的jstring变量。
    s = (*env)->NewStringUTF(env, "123");
    if(NULL == s)
        return;   
    //用新的jstring变量修改fieldID指定的成员变量的值。
    (*env)->SetObjectField(env, obj, sid, s);
}
运行程序的结果为:
In C:
 c.s = "abc"
In Java:
 c.s = "123"

4.1.1.1 访问成员变量的步骤

总的来说,访问类的成员变量需要两步:

1.通过GetFieldID()函数获得相关类的成员变量ID,这个类可以通过GetObjectClass()函数得到。

2.调用GetObjectField()函数读取引用类型的成员变量,调用Get<Primitive Type>Field()函数读取基本类型成员变量。调用SetObjectField()或/和Set<Primitive Type>Field()函数修改引用类型/基本类型成员变量的值

4.1.1.2 类型签名

在GetFieldID()函数中,最后一个参数是”Ljava/lang/String;”,它被称为类型签名(或类型说明符)。它由类的成员变量类型决定。可以用“I”表示int,用”D”表示double,用”F”表示float,用”Z”表示boolean等

如果成员变量类型为引用类型,那么生成的规则是:以“L”开头,后面跟上JNI类描述符,以分号”;”结尾,JNI类描述符中的点号”.”变为斜杠”/“。如String类型的类描述符是java.lang.String(String类的包名为java.lang.String),所以String的类型签名为:”Ljava/lang/String;”。

对于数组类型的成员变量,规则是:以“[“开头,后面跟上数组元素的类型描述符。如int[]的类型签名是”[I”,String[]的类型签名为”[L/java/lang/String;”。

4.1.2 访问静态成员变量

访问静态成员变量与访问非静态成员变量差不多,只是函数稍微修改了一下。下面是Java代码:

class StaticFieldAccess {
    //类静态成员变量 
    private static String s;
    //原生代码读取和修改成员变量的值 
    private native void accessField();

    public static void main(String[] args) { 
        StaticFieldAccess c = new StaticFieldAccess();
        //初始化 
        c.s = "abc";
       //原生代码 
       c.accessField(); 
       System.out.println("In Java:"); 
       System.out.println(" c.s = "" + c.s + """); 
    } 
    static { 
        System.loadLibrary("StaticFieldAccess"); 
    }
}
下面是原生函数的实现代码:
#include <jni.h>
#include "StaticFieldAccess.h"

JNIEXPORT void JNICALL Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj)
{
    //获取obj指定的类
    jclass clazz = (*env)->GetObjectClass(env, obj);
    if(NULL == clazz)
        return;
    //通过该类定义,找到该类的名称为s的成员变量,其类型签名为"Ljava/lang/String;",即String类型。注意这里函数修改为GetStaticFieldID()。
    jfieldID sid = (*env)->GetStaticFieldID(env, clazz, "s", "Ljava/lang/String;");
    if(NULL == sid)
        return;
    //获得fieldID后,通过obj读取该成员变量的值,为jstring类型。这里的函数修改为GetStaticObjectField(),并且第二个参数由obj该为clazz。
    jstring s = (*env)->GetStaticObjectField(env, clazz, sid);
    if(NULL == s)
        return;
    //这里就很熟悉了,将引用类型转换为C/C++相应类型。
    const char *ss = (*env)->GetStringUTFChars(env, s, NULL);
    if(NULL == ss)
        return;
    //打印
    printf("In C:n c.s = "%s"n", ss);
    //打印完成后,不再使用const char *ss,就释放掉。
    (*env)->ReleaseStringUTFChars(env, s, ss);
    //用新的值生成一个新的jstring变量。
    s = (*env)->NewStringUTF(env, "123");
    if(NULL == s)
        return;   
    //用新的jstring变量修改fieldID指定的成员变量的值。注意,这里的函数改为SetStaticObjectField(),并且第二个参数由obj该为clazz
。 (*env)->SetStaticObjectField(env, clazz, sid, s); }
运行结果与非静态成员变量一样:
In C:
 c.s = "abc"
In Java:
 c.s = "123"

4.1.2.1 访问静态成员变量的步骤

与访问非静态成员变量一样:

1.通过GetStaticFieldID()函数获得相关类的静态成员变量ID,这个类可以通过GetObjectClass()函数得到。

2.调用GetStaticObjectField()函数读取引用类型的静态成员变量,调用GetStatic<Primitive Type>Field()函数读取基本类型静态成员变量。调用SetStaticObjectField()或/和SetStatic<Primitive Type>Field()函数修改引用类型/基本类型静态成员变量的值

3.Get/SetStatic<Type>Field()系列函数的第二个参数是class类型。

4.2 访问类成员方法

JNI提供了很多函数允许从原生代码中调用Java的类成员方法。Java类成员方法由很多种,类静态成员方法、类非静态成员方法,特殊的有构造函数等。本节讨论调用静态成员方法和动态成员方法的相关JNI函数。

4.2.1 访问非静态成员方法

还是使用一个例子,下面是Java代码:

class InstanceMethodCall {
    private native void nativeMethod();
    //这是原生代码要回调的Java类成员方法。
    private void callback() {
        System.out.println("In Java.");
    }

    public static void main(String[] args) {
        InstanceMethodCall c = new InstanceMethodCall();
        c.nativeMethod();
    }

    static {
        System.loadLibrary("InstanceMethodCall");
    }
}
下面是原生代码的实现:
#include 
#include "InstanceMethodCall.h"

JNIEXPORT void JNICALL Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
    jclass clazz = (*env)->GetObjectClass(env, obj);
    if(NULL == clazz)
        return;
    jmethodID mid = (*env)->GetMethodID(env, clazz, "callback", "()V");
    if(NULL == mid)
        return;

    printf("In Cn");
    (*env)->CallVoidMethod(env, obj, mid);
}
执行的结果如下:
In C
In Java.

4.2.1.1 访问非静态类成员方法的步骤

1.使用GetMethodID()函数获取obj对应的类的方法ID,obj对应的类可以使用GetObjectClass()函数获得。这个方法根据传入的第三个参数(方法的名称)和第四个参数(方法的签名)在第二个参数(给定的类)中遍历查找。如果找到了匹配的方法,则返回该方法在该类中的ID,否则抛出NoSuchMethod异常。

2.使用Call<Return Type>Method()函数调用方法ID指定的类非静态成员方法。其中Return Type可以是int、float、double、boolean、Object等等。

4.2.1.2 Java方法的签名

在调用GetMethodID()函数的时候,第三个参数是Java中方法的名称,第四个参数是这个Java方法的签名(或描述符)。生成方法标签的规则是:以小括号”()”开头,小括号内是方法的参数的类型签名,多个参数用逗号”,”分隔,小括号后跟着方法返回值类型的签名。

如上个例子中的Java回调方法为:void callback() {….}。参数为空,返回值为void,所以该方法的标签为”()V”。

如Java方法为:String func(int, String[]) {….}。参数有两个,int和String[],返回值为String,所以该方法的标签为”(I,[Ljava/lang/String;)Ljava/lang/String;”。

 

下面的部分是05/17/2015完成的。

 

4.3 调用父类的非静态方法

可以调用在父类中定义并在子类中重写的非静态方法。JNI提供了一系列的CallNonvirtual<Type>Method()函数达到这个目的。步骤是:

1.使用GetMethodID()函数获得父类中定义的非静态方法的引用。

2.把jobject、父类的引用(jclass)、得到的方法ID(jmethodID)和其他的参数作为函数参数传递给CallNonvirtual<Type>Method()函数,如CallNonvirtualVoidMethod(), CallNonvirtualBooleanMethod(), CallNonvirtualIntMethod(), CallNonvirtualObjectMethod()等。

注意:在原生代码中使用CallNonvirtual<Type>Method()函数调用重写了的父类的非静态方法是比较少见的。这个功能类似于Java中调用重写的父类中的某个方法,如f():

super.f();

4.4 调用构造函数

在JNI中调用构造函数与调用其他非静态方法类似,步骤如下:

1.使用GetMethodID()函数获取构造函数的ID,其中第三个参数(函数的名字)为”<init>”,在函数签名中返回值为”V”。

2.可以把构造函数ID和其他JNI的函数(如NewObject())作为参数调用构造函数。

下面以一个例子介绍如何调用构造函数。这个例子调用Java中String类的String(char *)构造函数,使用给定的字符串构造一个新的String类。这个例子仅仅是为了演示如何调用构造函数,实际根本不会这么用。





jstring MyNewString(JNIEnv env, jchar chars, jint len)
{
jclass stringClass;
jmethodID cid;
jcharArray elemArr;
jstring result;
/使用FindClass函数找到java.lang.String类的引用/
stringClass = (env)->FindClass(env, “java/lang/String”);

if (stringClass == NULL) {
return NULL;
}
/以”<init>”为函数名,以”([C)V”为函数签名调用GetMethodID()函数,获取
java.lang.String类的String(char[])构造函数
/
cid = (env)->GetMethodID(env, stringClass,”<init>”, “([C)V”);

if (cid == NULL) {
return NULL; / exception thrown /
}
/
创建新的字符数组/
elemArr = (
env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL;
}
/把jchar数据从位置为0开始,一共len个字符写入到字符数组中/
(
env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/使用NewObject函数产生新的jstring对象,即java.lang.String类对象/
result = (env)->NewObject(env, stringClass, cid, elemArr);
/释放局部引用,可以让垃圾回收机制自动回收/
(
env)->DeleteLocalRef(env, elemArr);
(env)->DeleteLocalRef(env, stringClass);
/
返回新创建的jstring对象/
return result;
}





既然我们自己能够实现创建String类,那为什么JNI还内置提供了NewString()这样的函数呢?这是因为String类等非常常用,因此JNI有必要专门为了这些常用的类和对象创建经过优化了的JNI函数。JNI内置的函数经过特殊优化,比我们这里直接调用java.lang.String的构造函数要有效率的多。

上节学到的CallNonvirtual<Type>Method()函数也可以用来调用构造函数。对上述代码做一点点的修改,把下面这一句
/使用NewObject函数产生新的jstring对象,即java.lang.String类对象/
result = (
env)->NewObject(env, stringClass, cid, elemArr);

改为:




/调用AllocObject()函数生成未初始化的String类对象并分配空间/
result = (env)->AllocObject(env, stringClass);
if (result) {
/以字符数组、构造函数的ID、要创建的类型和上一步得到的分配到的空间为参数调用CallNonvirtualVoidMethod()函数,得到新的String类对象/
(
env)->CallNonvirtualVoidMethod(env, result, stringClass,cid, elemArr);

/如果发生了异常的话,删除为String类对象分配的空间,立即返回/
if ((env)->ExceptionCheck(env)) {
(
env)->DeleteLocalRef(env, result);
result = NULL;
}
}





原生代码不应该使用同一个对象参数调用构造函数多次,也就是说,在每个对象调用了原生代码后,该原生代码最多只能调用某个类型的构造函数一次

大多数情况,推荐使用NewObject()函数代替AllocObject()/CallNonvirtualVoidMethod()函数组合。因为后者更容易出错

# 4.5 缓存成员变量和成员方法的ID

调用JNI函数获取成员变量或成员方法的ID,是在Java类文件中通过变量/方法名和签名来遍历查找,如果在原生代码中多次引用同一个变量或方法的话,采用这种方法就会显得效率低且耗时多。因此,这一节讨论如何缓存它们的ID并在该原生代码结束前重复使用。

缓存ID分两种情况,下面分别介绍。

## 4.5.1 第一种情况(Caching at Point of Use)

这个不好翻译,大概意思就是说在原生代码中使用到了成员变量或成员方法的ID时再缓存。Java代码可能会调用同一个原生函数很多次,在这个原生函数中,只在第一次被调用时计算用到的Java类中的成员变量或成员方法的ID,并将ID保存到一个静态变量中,那么后续的所有调用都不用重新计算查找这些ID了。下面是一个简单的例子:




JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv env, jobject obj)
{
/
定义一个静态变量存储Java类成员变量的ID/
static jfieldID fid_s = NULL;
/
获取本原生函数所属的Java对象/
jclass cls = (
env)->GetObjectClass(env, obj);
jstring jstr;
const char str;
/
fid_s如果为NULL,说明是第一次调用本原生函数/
if (fid_s == NULL) {
/
第一次调用本原生函数,
在本原生函数所属的对象所属的类中查找String s;成员变量的ID/
fid_s = (env)->GetFieldID(env, cls, “s”,”Ljava/lang/String;”);

/如果找不到,就会自动抛出异常,立即返回。让Java异常处理捕获异常/
if (fid_s == NULL) {
return;
}

}
printf(“In C:n”);
/根据ID获取其内容,并转换为char/
jstr = (env)->GetObjectField(env, obj, fid_s);
str = (
env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return;
}
printf(“ c.s = “%s”n”, str);
(env)->ReleaseStringUTFChars(env, jstr, str);
/
把ID所代表的变量的值修改为”123”/
jstr = (
env)->NewStringUTF(env, “123”);
if (jstr == NULL) {
return;
}
(env)->SetObjectField(env, obj, fid_s, jstr);
}





虽然避免了每次都重新计算查找ID,但如果该原生函数在Java中在多个线程中同时调用时,就会有一个问题:竞争。多个线程同时调用这个原生函数,同时查找计算ID。一个线程可能覆盖另个一线程存储在静态变量中的ID。所幸的是,在不同线程中对同一个类的同一个成员变量或成员方法的ID查找结果是一样的,所以虽然多线程时不同线程可能会重复查找ID,但这不会带来麻烦。

基于这个知识,我们可以修改一下上面提到的MyNewString原生函数,把java.lang.String类的构造函数ID缓存起来:
jstring MyNewString(JNIEnv env, jchar chars, jint len)
{
jclass stringClass;
static jmethodID cid = NULL;
jcharArray elemArr;
jstring result;
/
使用FindClass函数找到java.lang.String类的引用/
stringClass = (
env)->FindClass(env, “java/lang/String”);
if (stringClass == NULL) {
return NULL;
}
/第一次调用时计算ID、后续调用可以直接使用。/
if(NULL == cid) {
/以”<init>”为函数名,以”([C)V”为函数签名调用GetMethodID()函数,获取
java.lang.String类的String(char[])构造函数
/
cid = (
env)->GetMethodID(env, stringClass,”<init>”, “([C)V”);
if (cid == NULL) {
return NULL; / exception thrown /
}

}
/创建新的字符数组/
elemArr = (env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL;
}
/
把jchar数据从位置为0开始,一共len个字符写入到字符数组中/
(env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/
使用NewObject函数产生新的jstring对象,即java.lang.String类对象/
result = (
env)->NewObject(env, stringClass, cid, elemArr);
/释放局部引用,可以让垃圾回收机制自动回收/
(env)->DeleteLocalRef(env, elemArr);
(
env)->DeleteLocalRef(env, stringClass);
/返回新创建的jstring对象/
return result;
}

红色部分代码是修改的部分。

## 4.5.2 第二种情况(Caching in the Defining Class’s Initializer)

第二种情况是在原生函数所属的类的静态初始化部分缓存ID。

在第一种情况中,每次调用原生函数都要检查静态缓存变量是否为NULL,判断ID是否已经被缓存。这种方法会带来多余的检查/计算查找ID的步骤,影响速度。比如,多线程时所有县城同时调用该方法,那么就会重复检查和计算ID N次。

在很多情况下,原生代码所需要的ID可以在应用程序还没有调用原生代码之前被缓存起来。Java虚拟机会在使用类的任何方法和变量之前执行这个类的静态初始化函数(或上下文)。因此,在这个静态初始化函数或上下文中计算查找ID并把它缓存起来,就相当于我们只计算了一次。不管是在多线程还是单线程,这个ID只会被计算查找一次,因为一个类的静态初始化只会出现一次。

比如,我们修改4.2.1节访问非静态成员方法中的例子,在静态初始化上下文中计算并缓存非静态方法的ID:
class InstanceMethodCall {
private native void nativeMethod();
/initIDs()用于在静态上下文中计算ID,并缓存/
private static native void initIDs();
//这是原生代码要回调的Java类成员方法。
private void callback() {
System.out.println(“In Java.”);
}

public static void main(String[] args) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}

static {
System.loadLibrary(“InstanceMethodCall”);
/在静态上下文中调用这个函数计算并缓存ID
这个函数会在InstanceMethodCall类使用任何成员前被调用。
这个函数只会被执行一次,不管单线程还是多线程。
/
initIDs();
}
}

红色部分就是新添加的两条语句,用于在静态上下文计算并缓存ID。initIDs()函数的实现如下:




/MID_InstanceMethodCall_callback是一个全局变量,用于保存方法ID/
jmethodID MID_InstanceMethodCall_callback;
JNIEXPORT void JNICALL Java_InstanceMethodCall_initIDs(JNIEnv env, jclass cls)
{
/
获取InstanceMethodCall.callback()方法的ID,保存在全局变量中。
只计算一次就可以在用到InstanceMethodCall.callback()方法的地方使用全局变量
/
MID_InstanceMethodCall_callback =
(env)->GetMethodID(env, cls, “callback”, “()V”);
}





因为initIDs()函数是在静态上下文中调用,是在类调用任何成员方法(如nativeMethod()或main())之前执行的,所以后面调用的方法如果要使用callback()的ID,就可以直接使用MID_InstanceMethodCall_callback了。如nativeMethod()方法的实现代码如下:




JNIEXPORT void JNICALL Java_InstanceMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
printf(“In Cn”);
/直接使用MID_InstanceMethodCall_callback调用callback(),不用再计算了。/
(*env)->CallVoidMethod(env, obj, MID_InstanceMethodCall_callback);
}




4.5.3 两种方法的比较

当程序员不能修改类的定义的时候,使用第一种方法(ID缓存在静态变量中)是合理的。比如,我们自己写的MyNewString原生代码就不能在java.lang.String类的静态上下文中添加initIDs()方法,这时就只能使用第一种方法了。

然而,相比第二种方法,第一种方法有一些弊端

1.前面提到过,多线程时会重复检查和计算ID,带来效率和性能问题。

2.第一种方法把ID缓存在静态变量中,这时变量中的值只在原生代码所属的类没有被卸载(unloaded)时有效。一旦Java虚拟机卸载了该类,又重新加载了该类,那么这个静态变量中的值就不再有效,如果继续引用就很可能出错。然而,第二种方法是在类的初始化静态上下文中缓存ID到全局变量,如果类被卸载又重新加载的话,这个静态上下文的部分还会重新执行一次,所以ID也会被重新计算和缓存,所以继续引用全局变量不会出错

因此,尽可能的使用第二种方法缓存ID

4.6 JNI调用成员变量和成员方法的性能

使用JNI技术就是为了提高Java程序的性能,那么JNI中回调Java方法(native->Java)、Java调用原生函数(Java->native)和Java调用Java函数(Java->Java)的性能分别如何呢?

JNI的性能与Java虚拟机底层如何实现JNI技术有关系,因此对上述问题不可能给出具体的数值,但我们根据内在的原理来推论上述问题的答案。

4.6.1 Java->native 与 Java->Java的比较

Java->native很可能比Java->Java要慢,原因如下:

1.原生函数很可能遵循不同的函数调用规则(如参数传递顺序),因此Java->native调用时,需要对参数和函数栈等会做额外的转换操作,以便符合原生函数的调用规则。而在同一个Java虚拟机中,Java->Java调用就不存在规则不同了,不用多余的转换。

2.在Java->Java调用时,允许inline调用,非常迅速快捷。但是Java->native调用使用inline就非常困难。

在典型的Java虚拟机实现中,一般情况下Java->native比Java->Java调用要慢2-3倍。因此从性能角度考虑,除非遇到了Java不能完成的任务或Java完成起来很麻烦很困难的任务,否则尽可能的不用JNI。当然,Java虚拟机可以在实现JNI时让其调用规则符合Java->Java调用,这样Java->native与Java->Java的速度就非常接近了。

4.6.2 Java->native 与 native->Java的比较

这两种情况的性能差不多

4.6.3 native->Java 与 Java->Java的比较

理论上来说,native->Java应该比Java->Java慢2到3倍。但实际上,可能慢10倍左右。这是因为native->Java(回调)非常的罕见,所以Java虚拟机在实现的时候并没有对这种回调做优化。

4.6.4 JNI访问成员变量和成员方法的性能

在原生代码中访问成员变量或成员方法的性能主要依赖JNIEnv接口所指的JNI函数的性能。一般来说,性能还是能接受的,因为调用JNI优化过的函数只需要花费几个周期。

 

哇,这一章终于介绍(翻译)完了,码字好累。不过,写完这篇文章相当于又复习了一遍,常读常新,对JNI理解会更多。有问题可以发邮件xinspace@yeah.net或回复留言。