JNI官方文档翻译4-属性和方法的访问

来源:互联网 发布:php商品分类查询 编辑:程序博客网 时间:2024/05/16 01:40

    本篇文章介绍如何访问任意对象的属性和方法,当然是在native层访问,方法的访问一般作为java层的回调来访问。我们先从 属性的访问和回调函数的访问开始,接下来再讨论一下使用一种高效简单的缓存技术来提高效率。最后我们讨论native访问java层属性和方法的性能特点。


属性的访问:

Java语言支持两种属性,每个实例都有自己独立的属性,所有实例共享同一份静态属性。JNI提供get set 系列方法来访问静态属性和非晶态属性。

请看如下代码片段:

class InstanceFieldAccess {    private String s;//非静态属性    private native void accessField();//本地方法声明    public static void main(String args[]) {        InstanceFieldAccess c = new InstanceFieldAccess();        c.s = "abc";//set s to "abc"        c.accessField();//本地方法调用,改变字符串的值        System.out.println("In Java:");        System.out.println(" c.s = \"" + c.s + "\"");    }    static {        System.loadLibrary("InstanceFieldAccess");    }}

我们看一下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);//步骤1    printf("In C:\n");    /* Look for the instance field s in cls */    fid = (*env)->GetFieldID(env, cls, "s","Ljava/lang/String;");//步骤2    if (fid == NULL) {        return; /* failed to find the field */    }    /* Read the instance field s */    jstr = (*env)->GetObjectField(env, obj, fid);//步骤3,因为字符串是引用类型    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"

访问非静态属性需要一些固定的步骤 1.etObjectClass 2.GetFieldID,3,GetObjectField , 这个步骤有点类似于java层的反射调用。

JNI也支持GetIntField  、SetFloatField等。 你可能注意到"Ljava/lang/String;",这个是JNI属性描述符。

下面解释一下描述符的含义:

L代表引用类型,你可以记做Language,属性是引用类型的都以这个字符开始,紧接着就是包名,只是  “.”被 “/”代替

Z代表boolean , 你可以记做Zero for short

数组的描述符是[    你可以记做 [ ], I[ 就是int[]

F代表float ,I代表int等,这个描述符不需要记忆,了解即可,有一个工具可以帮我们生成这个描述符:

javap -s -p InstanceFieldAccess 你将得到如下输出片段:

...
s Ljava/lang/String;
...

一般我们推荐使用工具,可以帮我们避免错误。


我们看看如何访问静态方法:java 层

class StaticFielcdAccess {    private static int si;//static i for short.    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");    }}

native层:

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);//拿到class    printf("In C:\n");    /* Look for the static field si in cls */    fid = (*env)->GetStaticFieldID(env, cls, "si", "I");//拿到fieldID    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.API调用不同,静态属性使用GetStaticFieldID ,非静态属性使用GetFieldID ; 2, API传的参数不同GetStaticIntField传的是jclass

GetObjectField 传的是jobject。自己对比区别一下。



访问方法,同样分两种,静态方法和非静态方法:

访问实例方法的例子:java层

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);//步骤1    jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");//步骤2    if (mid == NULL) {        return; /* method not found */    }    printf("In C\n");    (*env)->CallVoidMethod(env, obj, mid);//步骤3}

同样,3步骤

输出结果:

In C
In Java

如果GetMethodID返回NULL 则NoSuchMethodError就会被抛出, CallVoidMethod 传入的是jobject, JNI有一族函数:

Call<Type>Method Type可以使Object Void, Int等

你可能注意到了“()V” 这个是方法描述符,你可以通过工具来生成:

javap -s -p InstanceMethodCall    你将得到如下输出:

...
private callback ()V
public static main ([Ljava/lang/String;)V
private native nativeMethod ()V
...


简单解释一下这个描述符的含义

native private String getLine(String); 的描述符是"(Ljava/lang/String;)Ljava/lang/String;" ,括号里的是参数,后面的是返回值类型

public static void main(String[] args); 的描述符是"([Ljava/lang/String;)V"




访问静态方法:这里只贴出代码,不做解释,同访问今天太属性一个道理:

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");    }}

JNIEXPORT void JNICALL Java_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj){    jclass cls = (*env)->GetObjectClass(env, obj);    jmethodID mid =    (*env)->GetStaticMethodID(env, cls, "callback", "()V");//拿到methodID    if (mid == NULL) {        return; /* method not found */    }    printf("In C\n");   (*env)->CallStaticVoidMethod(env, cls, mid);//传入jclass}

输出如下:

In C
In Java


下面介绍一个比较有意思的,访问父类的方法,你会看到C++的样子:

JNI提供一族API CallNonvirtual<Type>Method 来访问父类的方法,对于子类来说,子类继承父类,并继承父类的方法,但是,对于JNI来说需要区分哪些是子类复写override的,哪些没有被复写的。在c++中,有虚函数的概念,可以对比一下。其他的参照访问静态方法和非静态方法。对于方法来说,有父类和子类的区别,对于属性来说,似乎没有,都使用同一套API。

CallNonvirtualVoidMethod 可以用来访问父类的构造函数。请看如下native代码,调用构造方法返回一个字符串

jstring MyNewString(JNIEnv *env, jchar *chars, jint len){    jclass stringClass;    jmethodID cid;    jcharArray elemArr;    jstring result;    stringClass = (*env)->FindClass(env, "java/lang/String");//找到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");//获取构造方法的MethodID    if (cid == NULL) {        return NULL; /* exception thrown */    }    /* Create a char[] that holds the string characters */    elemArr = (*env)->NewCharArray(env, len);//new一个char[] 作为临时变量    if (elemArr == NULL) {        return NULL; /* exception thrown */    }    (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);//将临时变量赋值,将传入的char* 拷贝到新elemArr    /* 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;}

这个例子比较复杂,值得详细解释一下:GetMethodID 实际上是获取的String(char[] chars).构造函数的方法,作为构造方法,返回值是void,因为java层构造方法没有返回值。

DeleteLocalRef我们下一节再介绍。我们之前好像有类似的生成字符串的方法NewString系列,这也是一种生成字符串的方法,但是前者更便捷高效,String也是很常用的,因此JNI单独设计了一套API来支持字符串的操作。

下面的代码片段

result = (*env)->NewObject(env, stringClass, cid, elemArr);

它可以是另一种形式:使用AllocObject创建一个 “未初始化的” 对象,也就是分配了内存,但是没有初始化。你只能在这块内存上调用一次构造方法,且只能一次。不调用,或者调用多次都会导致错误。

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;    }}
这种形式的使用方式很容易导致错误,用法有些复杂,所以我们最好使用NewString系列来操作字符串。



缓存方法和属性的ID

我们获取方法和属性的ID都需要查找符号表,这个查找是相当耗时的,代价略高。因此当查找完毕后缓存复用将会提高效率。有两种缓存方法,1 使用的时候缓存,2通过静态代码块缓存,下面分别介绍这两种方法:

1,使用时缓存

JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj){    static jfieldID fid_s = NULL; /* cached field ID for s , 这里是关键,使用static, 只有第一次调用初始化*/    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);//复用缓存}

这个方法在多线程下会导致竞争问题,结果就是重复初始化,但是重复的初始化不会导致程序运行不正确,没什么损害。


2.在静态代码块里进行初始化,java层代码:

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();    }}

native层代码:

jmethodID MID_InstanceMethodCall_callback;//全局变量JNIEXPORT void JNICALL Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls){    MID_InstanceMethodCall_callback = (*env)->GetMethodID(env, cls, "callback", "()V");}

很明显这种方式使用了全局变量, 下次在使用中方法id的时候,直接:

JNIEXPORT void JNICALLJava_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj){    printf("In C\n");    (*env)->CallVoidMethod(env, obj, MID_InstanceMethodCall_callback);}

两种缓存方法的比较:

运行时缓存策略需要一次或多次check 和init。

Method 和Field IDs 会一直有效,直到class被卸载。如果你使用运行时缓存策略,那你必须保证class不被卸载再装载,换句话说,在你的native方法还依赖这个缓存的id之前,你的class 不能被卸载再装载,下一章会将到,怎么样保证你的class不被卸载。 如果使用静态代码块的方式缓存,那么class被卸载再装载后,这个id都会被重新计算。因此推荐使用静态代码块的方式。




通过JNI的方式操作属性和方法的性能情况:

native方法访问Java方法   native方法访问native方法   java方法访问java方法, 这三种方式,哪种最高效呢????

这个问题依赖于虚拟机实现JNI的方式,在这里我们只讨论固有的开销,只讨论一般的情况。

一般情况下java/native 调用要比java/java调用效率略低一些,因为:native方法很可能遵循一种新的调用规则,结果是,虚拟机必须对这种变化做出适当的转换,比如构造一些新的数据结构设置堆栈等等,内联java/native方法要比内联java/java方法要复杂。粗略的测试了一下,java/native 调用要比java/java慢2-3倍, native/java 调用同 java/native调用一样,也会慢。实际当中,native回调java方法的情况并不多见,虚拟机也不会经常优化回调java方法的性能,文档上说,写这个文档的时候,native回调java方法,要比java调用java方法慢10倍。可见开销有多大,所以如果不是特别有必要,我们最好不要让native方法去调用java层方法。

然而访问java层的属性就没有这么大的差别,可以忽略不计,原因不翻译了,记住结论即可。



上一篇:JNI官方文档翻译3-基本数据类型 字符串 数组


下一篇:JNI官方文档翻译5-局部和全局引用



0 0
原创粉丝点击