JNI之缓存与引用

393 阅读5分钟

导读

在前面《JNI之访问java属性和方法》 一文中我们介绍了在JNI方法中访问java类对象的属性和方法,试想一下如果每次调用JNI方法都经过查找类,查找方法id,最后完成调用,这里是否有性能问题呢? 而且FindClass/GetMethodID/GetFieldID这些函数本身就含有一些性能问题,既然如此,那能否将FindClass/GetMethodID/GetFieldID这些函数的调用结果缓存起来,下次再进入时直接使用缓存即可呢?肯定是可以的。

JNI引用类型

在JNI中包含三种引用类型,它们分别是全局引用、局部引用和弱全局引用。

1、局部引用 局部引用又称为本地引用,局部引用会在函数结束时自动释放,例如函数FindClass返回的就是一个局部引用。需要注意的是局部引用的个数是有限的,一般是512个,所以局部引用能手动释放的尽早手动进行释放,特别是在一些循环体内,使用完毕马上进行释放,否则很可能会超出局部引用个数的限制。

同时局部引用还会阻止对象被回收,也就是影响GC,所以更应该在不需要时尽早进行释放,通过函数DeleteLocalRef可以释放局部引用。

不能在本地方法中通过静态变量来储存本地引用,并在后续调用中使用相同的引用,因为本地引用是会被自动释放的。

局部引用仅仅在创建它的线程中是有效的。在线程A中被创建的局部引用是不能够在线程B中被使用的。千万不要将一个局部引用保存在全局变量中给其他线程使用。

如果你使用AttachCurrentThread连接(attach)了Native进程,正在运行的代码在线程分离(detach)之前绝不会自动释放局部引用。使用者创建的任何局部引用必须手动删除。

2、全局引用 全局引用不会在函数结束时自动释放,如果没有释放则会在程序运行期间一直保留,所以特别适合用来做一些缓存的操作。

不同于局部引用,全局引用是可以跨线程使用的,只要它不被程序员手动释放就会一直有效。

通过函数NewGlobalRef可以创建一个全局引用,当你的本地代码不再需要访问一个全局引用时,你应该调用DeleteGlobalRef方法。如果你忘记调用这个函数,虚拟机将无法通过垃圾收集器回收相应的对象,即使这个对象再也不会在系统的其他地方中使用。

注意jfieldID和jmethodID是映射类型(opaque types),不是对象引用,不应该被传入到NewGlobalRef。原始数据指针,像GetStringUTFChars和GetByteArrayElements的返回值,也都不是对象(它们能够在线程间传递,并且在调用对应的Release函数之前都是有效的)。

3、弱全局引用 弱全局引用不像局部引用哪样在函数结束时自动释放,也不像全局引用哪有一直保留在内存中,弱全局引用在GC的时候是可能会被垃圾回收的,因此每次在使用弱全局引用之前都需要进行判空处理。可以通过函数IsSameObject与NULL对比判断当前弱全局引用是否还有效。绝不要在Native代码中用==符号来比较两个引用。

通过函数NewWeakGlobalRef可以创建一个弱全局引用,当你的本地代码不再需要访问一个弱全局引用时,你应该调用DeleteWeakGlobalRef函数释放掉弱全局引用。如果你忘记调用这个函数,Java虚拟机仍然能够通过垃圾收集器收集底层对象,但是将无法回收该弱全局引用对象占用的内存。

缓存策略

通过上面JNI三种引用的介绍我们知道,如果需要做缓存的话,使用全局引用就特别适合了。

平时我们在做缓存策略的时候一般会面临着两种选择,一种是类似懒加载式的缓存策略,就是在第一次调用时如果为空则进行缓存;另外一种是在静态代码块中进行缓存。那么这两种缓存策略又什么区别呢,孰优孰劣呢?

针对第一次使用时缓存判空缓存策略需要在每一次使用时对相关资源进行判空校验,且可能在多线程使用状况下产生线程安全的问题;而且这种策略仅仅在类没有被卸载的时候是有效的,开发者必须保证在你的代码还依赖你所缓存的这些ID的过程中,你所使用的类没有被卸载或者重新加载。

使用类静态初始化式缓存策略,被缓存的资源在类被卸载并重新加载时候会被自动重新计算,并且在多线程情况下不会有线程安全的问题。因此建议使用类的静态初始化代码段中计算并缓存属性或者方法ID等相关资源。

以下是一个使用类静态代码块缓存属性ID的一个简单demo:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("jnitest");
    }

    private int age = 16;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.sample_text);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                age++;
                Person person = new Person(age);
                person.changeAge();
                person.printAge();
            }
        });
    }

}

Person.java

public class Person {

    private int age;

    Person(int age){
        this.age = age;
    }

    public void printAge(){
        Log.v("Person_Tag","age:" + age);
    }

    static {
        initIDs();
    }
    // 初始化缓存id
    private native static void initIDs();
    // 通过JNI修改年龄
    public native void changeAge();
}

native-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_com_fly_jnitest_Person_initIDs(JNIEnv *env, jclass clazz) {
    // 缓存,弱全局引用
    ageFieldID = env->GetFieldID(clazz,"age","I");
}

extern "C"
JNIEXPORT void JNICALL
Java_com_fly_jnitest_Person_changeAge(JNIEnv *env, jobject thiz) {
    if(nullptr != ageFieldID){
        jint age = env->GetIntField(thiz,ageFieldID);
        env->SetIntField(thiz,ageFieldID,age + 1);
    }
}

系列推荐

JNI基础简介
JNI之数组与字符串的使用
JNI之动态注册与静态注册
JNI之访问java属性和方法

关注我,一起进步,人生不止coding!!!

公粽号:思想觉悟