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-局部和全局引用

时间: 2024-10-10 02:33:20

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

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

在使用JNI的时候,你问的最多的问题莫过于 Java的数据类型和C/C++的数据类型怎么一对一映射.在我们的HelloWord例子当中,我们并没有传入任何参数给我们的java层print方法,native方法也并没有返回任何数据而是void,本地方法只是简单的打印一个字符串,然后就返回了.实际开发中我们都需要传入参数,返回参数,本章就会讨论如何从java层向底层传数据,以及如何从底层向java层返回数据.我们从基本数据类型 字符串 数组开始, 下一章再介绍如何传任意类型的数据,以及如何访问他们的

Solidity属性和方法的访问权限

属性:默认是internal的类型,外部是不可以访问调用的,如果加上public的话,那么是会自动为这个属性加上一个get的方法的,比如uint   public _age; => function _age() {} 自动生成 方法:默认是public的类型,外部是可以直接访问调用的 public:,不管是属性还是方法都可以通过合约地址的形式来进行访问,this.xxxxx()的形式,public属性:_age(),public方法:tets2()internal,private:,就不用加t

Python3.2官方文档翻译--实例对象和方法对象

6.3.3 实例对象 如今我们用实例对象做什么呢?实例对象唯一可用的操作就是属性引用.如今有两种合法的属性名称:数据属性和方法. 数据属性相当于smallTalk中的实例变量,C++中的数据成员.数据属性不须要申明.像局部连梁一样,当他们初次赋值的时候他们就存在了.比如,假设x是上面创建MyClass类的一个实例, 以下的代码块表示将会打印值16.这个值没有不论什么错误. x.counter = 1 while x.counter < 10: x.counter = x.counter*2 pr

【速记速学】Python类的定义,属性,方法,访问性!

前言: Python 随着人工智能,机器学习,深度学习,AI 的发展,迅速成为宠儿. 今天,花 5 分钟时间,解读下Python中的类,定义,使用等. 如何定义自己的类呢? 01 类(对象) class dog(object)以上定义了一个dog对象,它继承于根类object. 02 类的属性 def init(self, name, dtype):self.name = nameself.dtype = dtype以上定义了dog对象的两个属性:name, dtype,通过init,这个系统函

Objective-C 类属性和方法的访问权限

OC中提供了4种访问权限,@private, @public, @protected这三种和其他的C++, Java是一样的,@package这个访问权限并不是Java里的包访问权限,OC中没有包的概念,这个是框架级的访问权限,在当前的framework的类中视为@protected,在框架以外的类中访问被视为@private. // // Goods.h // 05_Self // // Created by apple on 14-11-10. // Copyright (c) 2014年

python3 类的属性、方法、封装、继承及小实例

Python 类 Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,方法中可以调用基类中的同名方法. 对象可以包含任意数量和类型的数据. python类与c++类相似,提供了类的封装,继承.多继承,构造函数.析构函数. 在python3中,所有类最顶层父类都是object类,与java类似,如果定义类的时候没有写出父类,则object类就是其直接父类. 类定义 类定义语法格式如下: class ClassName:    <statem

Python3.2官方文档翻译--标准库概览(一)

7.1 操作系统接口 Os模块提供主要许多与操作系统交互的函数. >>> import os >>> os.getcwd() # Return the current working directory 'C:\\Python31' >>> os.chdir('/server/accesslogs') # Change current working directory >>> os.system('mkdir today') # R

Alljoyn瘦客户端库介绍(官方文档翻译)

Alljoyn瘦客户端库介绍(1) 1.简介 本文档对AllJoynTM瘦客户端的核心库文件(AJTCL)进行了详尽的介绍.本文档介绍了系统整体架构,AllJoyn框架结构,并着重于介绍如何将嵌入式设备加入AllJoyn系统整体架构中.1.1目的 本文档介绍了如何使一个受限于功耗.计算能力和内存的设备(嵌入式设备)加入AllJoyn分布式系统.具体而言,本文档包括了对AllJoyn面向嵌入式系统的方面的介绍,并着重描述了基于AllJoyn的系统的各个组件是如何与嵌入式设备协作以构建一个基于接近式

Swift语言官方文档翻译(2)

A Swift Tour 按照惯例,我们一般学习一个新语言的时候,我们都习惯性的在屏幕上打印"Hello, World",在Swift中,你可以用如下一个单独语句实现 println("Hello,World") 如果你用C或者OC写过程序,那么上面的语句对于你来说是很熟悉的.在Swift中,这一行代码就是一个完整的程序,你不需要为了类似I/O或者String handling去导入一些jar包.全局变量将作为一个程序的入口点,所以你不需要main函数,你也不需要在