为什么使用缓存技术?使用JNI接口访问字段或方法时,需要通过字段或方法的ID来进行操作,获取ID的过程是一个检索的过程,该过程相对而言比较耗时,缓存技术就是为了减少该过程的时间消耗。缓存技术又根据发生时刻不同分为两种:使用时缓存(Caching at the Point of use)和类初始化缓存(Caching in the Defining Class’s Initializer)。
本文通过两个演示程序分别介绍这两种缓存技术的使用方法,并在最后通过一个对比程序验证缓存所带来的速度提升。
字段和方法的ID可以在访问或者被回调的时候缓存起来,在Native层使用JNI定义的数据类型调用Java的相关字段或者方法时,可以将存储ID的数据类型设置为静态的,这样本地方法重复调用时,不必重新搜索字段ID
public class JNICacheIni{
static{
System.loadLibrary("JNITest");
}
public native void cacheMethod();
private String str = "I am a String from Java!";
public static void main(String args[]){
JNICacheIni jnic = new JNICacheIni();
jnic.cacheMethod();
System.out.println("After calling JNI function:");
System.out.println(jnic.str);
}
}
JNIEXPORT void JNICALL Java_JNICache_cacheMethod
(JNIEnv *env, jobject obj){
jclass jcla = env->GetObjectClass(obj);
jboolean jb = JNI_TRUE;
// 使用id时,设置为static
static jfieldID jfid = env->GetFieldID(jcla, "str", "Ljava/lang/String;");
jstring jstr = (jstring)env->GetObjectField(obj, jfid);
const char*str = env->GetStringUTFChars(jstr, &jb);
cout << "In C++:" << endl;
cout << str << endl;
const char *new_str = "I am a string from C++.";
jstring new_jstr = env->NewStringUTF(new_str);
env->SetObjectField(obj, jfid, new_jstr);
}
VM在调用一个类的方法和字段之前,都会执行类的静态初始化过程,在静态初始化的过程中缓存要访问的字段或方法ID是一个不错的选择。
我们可以在类初始化的过程中增加一个Native函数,该函数获取目标ID并保存为全局变量, 这样在Java层对象访问JNI对应的ID时,不必再重新搜索。
public class JNICacheIni{
static{
System.loadLibrary("JNITest");
initIDs();
}
public native static void initIDs();
public native void nativeMethod();
private void callBack(){
System.out.println("In Java");
}
public static void main(String args[]){
JNICacheIni jnic = new JNICacheIni();
jnic.nativeMethod();
}
}
jmethodID JNICacheIni_initIDs_jmid;
JNIEXPORT void JNICALL Java_JNICacheIni_initIDs
(JNIEnv *env, jclass jcl){
JNICacheIni_initIDs_jmid = env->GetMethodID(jcl, "callback", "()V");
}
JNIEXPORT void JNICALL Java_JNICacheIni_nativeMethod
(JNIEnv *env, jobject obj){
cout << "In C++:" << endl;
env->CallVoidMethod(obj, JNICacheIni_initIDs_jmid);
}
下面通过一个对比程序来验证缓存带来的速度提升,通过成员函数、使用缓存的JNI函数和不使用缓存的JNI函数,分别访问一个成员变量1024*1024次,并在函数内完成累加,修改成员变量的值,完成一次交互,最后统计时间。
public class JNICacheTest{
static {
System.loadLibrary("JNITest");
initsID();
}
private native void addCount();
private native void addCountByCache();
private native void addCountByInits();
private native static void initsID();
private int count;
private final int MAX_TIMES = 1024*1024;
public void add(){
count++;
}
public void setCount(int count){
this.count = count;
}
public static void main(String args[]){
JNICacheTest jnict = new JNICacheTest();
long startTime = System.currentTimeMillis();
for(int i=0;i<jnict.MAX_TIMES;i++){
jnict.add();
}
long time = (System.currentTimeMillis() - startTime);
System.out.println("Member function:"+time+"ms");
jnict.setCount(0);
startTime = System.currentTimeMillis();
for(int i =0;i<jnict.MAX_TIMES;i++){
jnict.addCount();
}
time = (System.currentTimeMillis() - startTime);
System.out.println("JNI function:"+time+"ms");
jnict.setCount(0);
startTime = System.currentTimeMillis();
for(int i =0;i<jnict.MAX_TIMES;i++){
jnict.addCountByCache();
}
time = (System.currentTimeMillis() - startTime);
System.out.println("JNI cache function:"+time+"ms");
jnict.setCount(0);
startTime = System.currentTimeMillis();
for(int i =0;i<jnict.MAX_TIMES;i++){
jnict.addCountByInits();
}
time = (System.currentTimeMillis() - startTime);
System.out.println("JNI initsID function:"+time+"ms");
}
}
jfieldID JNICacheTest_initsID;
JNIEXPORT void JNICALL Java_JNICacheTest_addCountByInits
(JNIEnv *env, jobject obj){
jint count = env->GetIntField(obj, JNICacheTest_initsID);
count++;
env->SetIntField(obj, JNICacheTest_initsID, count);
}
JNIEXPORT void JNICALL Java_JNICacheTest_initsID
(JNIEnv *env, jclass jcl){
JNICacheTest_initsID = env->GetFieldID(jcl, "count", "I");
}
JNIEXPORT void JNICALL Java_JNICacheTest_addCount
(JNIEnv *env, jobject obj){
jclass jcl = env->GetObjectClass(obj);
jfieldID jid = env->GetFieldID(jcl, "count", "I");
jint count = env->GetIntField(obj, jid);
count++;
env->SetIntField(obj, jid, count);
}
JNIEXPORT void JNICALL Java_JNICacheTest_addCountByCache
(JNIEnv *env, jobject obj){
jclass jcl = env->GetObjectClass(obj);
static jfieldID jid = env->GetFieldID(jcl, "count", "I");
jint count = env->GetIntField(obj, jid);
count++;
env->SetIntField(obj, jid, count);
}
不难看出,最快的还是Java-Java访问,然后是Java-JNI缓存,最后是Java-JNI,而且使用了缓存技术之后,速度提升了80%之多,由此可见,在访问JNI接口频率较高的程序中,缓存的使用必不可少。
其实两者没有本质的区别,都是将之前的jmethodID和jfieldID类型的变量设置成静态的,根据发生的时刻不同,再分为使用时和类静态初始化时,目的都是为了搜索ID的时间消耗。
使用时缓存技术不需要更改Java源代码,代码量也比较少,程序员需要对每一个ID变量进行检查,如果需要访问的ID比较少的话,这会是一个不错的选择。
如果能够对Java源代码进行更改的话,推荐优先使用类初始化的缓存技术,这样做更有利于ID的管理和使用。而且,方法和字段的ID依赖于类的存在,使用类初始化进行缓存,将会动态的更新ID值。