JNI官方文档翻译4-属性和方法的访问
Posted mtaxot
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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官方文档翻译4-属性和方法的访问的主要内容,如果未能解决你的问题,请参考以下文章