本篇文章介绍如何访问任意对象的属性和方法,当然是在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层的属性就没有这么大的差别,可以忽略不计,原因不翻译了,记住结论即可。