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

    本篇文章介绍如何访问任意对象的属性和方法,当然是在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 JNICALL
Java_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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值