·  阅读

Java 和 JNI 之间,传递基本类型对象的方式是值复制,而传递引用类型对象的方式是通过引用。在 JNI 中,有二种引用,局部引用(Local Reference)、全局引用(Global Reference)。弱全局引用(Weak Global Reference)是全局引用的一种特殊形式。

局部引用只有在JNI层的函数调用期期间有效,函数返回后,局部引用会被自动释放。局部引用会阻止垃圾收集器回收底层对象,只有当函数返回或者手动释放局部引用,垃圾收集器才有可能回收底层对象。

虽然说局部引用会被自动释放,但是我们最好还是手动释放,并且在某些极端情况下,为了不造成内存紧张,还是需要手动释放。例如

  • 在JNI层的方法中,引用了一个占用很大内存的Java对象,然后对这个对象执行一些操作,然后再执行一些与这个对象无关的操作,最后返回。这个Java对象在JNI层被引用期间,是无法被垃圾回收的,那么当我们执行完了与这个对象相关的操作后,最好立即释放这个局部引用,然后再执行一些与这个对象无关的操作,最后函数返回。
  • 当在JNI函数中,通过循环创建大量的局对象时,最好及时释放局部引用,否则可能造成OOM。
  • 那么如何释放局部引用呢?可以通过如下函数

    void DeleteLocalRef(JNIEnv *env, jobject localRef);
    

    我在 Android 源码中找到一个释放局部引用的例子

        virtual status_t scanFile(const char* path, long long lastModified,
                long long fileSize, bool isDirectory, bool noMedia)
            // 创建一个局部引用
            jstring pathStr;
            if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
                mEnv->ExceptionClear();
                return NO_MEMORY;
            // 使用刚创建的局部引用作为参数,调用Java对象的方法
            mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                    fileSize, isDirectory, noMedia);
            // 局部引用已经不再需要,立即释放它
            mEnv->DeleteLocalRef(pathStr);
            return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    

    从这个源码例子可以看出,尽管不手动释放这个局部引用也没啥影响,但是源码中还是通过DeleteLocalRef()手动释放。源码都尚且如此,何况我们一个普通的开发者呢?所以,在JNI函数中创建了局部引用,使用完毕后,手动释放这个局部引用。

    其实局部引用还可以通过如下函数手动创建

    jobject NewLocalRef(JNIEnv *env, jobject ref);
    

    参数ref可以是全局引用、弱全局引用或局部引用。目前我还不清楚为全局引用、局部引用创建局部引用有什么用,但是为弱全局引用创建局部引用还是有用的,这个作用会在文章后面介绍。

    NI函数在返回后,局部引用终究是会被释放。然而有时候我们需要把Java层传下来的对象进行保存,然后在函数返回后的某个时刻再进行操作。此时就需要使用如下函数为这个对象创建一个全局引用。

    jobject NewGlobalRef(JNIEnv *env, jobject obj);
    

    参数 obj 可以是一个局部引用,也可以是一个全局引用。这个函数执行成功会返回一个引用,然而如果发生OOM,返回NULL。

    全局引用在不需要的时候,必须手动释放,使用的函数原型如下

    void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
    

    Android 源码中有如下一个例子

        MyMediaScannerClient(JNIEnv *env, jobject client)
            :   mEnv(env),
                mClient(env->NewGlobalRef(client)), // 创建全局引用 
        virtual ~MyMediaScannerClient()
            // 释放全局引用
            mEnv->DeleteGlobalRef(mClient);
    

    MyMediaScannerClient 的构造函数的参数 client 是一个Java层的对象,它通过 NewGlobalRef() 函数创建一个全局引用,并用 mClient 变量保存。有了这个全局引用,就可以阻止垃圾收集器对它进行回收,这样就可以随时安全操作这个Java对象。

    当 MyMediaScannerClient 销毁时,会调用它的析构函数,此时就调用 DeleteGlobalRef() 来删除这个全局引用。这个全局引用被释放后,JNI层就不再阻止这个对象的垃圾回收。

    弱全局引用

    弱全局引用(Weak Global References)是一种特殊的全局引用,当一个底层Java对象只被弱全局引用所指向,这个弱全局引用不会阻止垃圾收集器回收这个底层Java对象。

    我们可以通过如下函数来创建弱全局引用

    jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
    

    当参数obj为NULL或者发生OOM(虚拟机会抛出OOM异常)时,函数返回NULL。

    弱全局引用的目的很显示,那就是不阻止垃圾回收,一般是为了防止内存泄露。

    弱全局引用也需要一定的虚拟机资源,因此在不需要弱全局引用时,需要使用如下函数释放

    void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
    

    既然弱全局引用不会阻止垃圾回收,那么它指向的底层对象可能被释放,因此在使用它时,我们最好使用下面的函数来把弱全局引用与NULL进行比较

    // 测试两个引用是否指向相同的Java对象
    // 如果两个引用指向相同的Java对象,或者两个引用都为NULL,
    // 函数返回JNI_TRUE,否则返回JNI_FALSE
    jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
    

    你可能还没有发现,弱全局引用的类型为jweak,那么如何使用它呢?其实我们只要搞清楚jweak和jobject的关系就行,代码如下

    class _jobject {};
    typedef _jobject*       jobject;
    typedef _jobject*       jweak;
    

    原来,jobject和jweak都是指针,而且都指向同一种类型的对象,因此把jweak当作jobject来用就行了。

    我们来看看Android源码中如何使用这个弱全局引用的

    JMediaCodec::JMediaCodec(
            JNIEnv *env, jobject thiz,
            const char *name, bool nameIsType, bool encoder)
        // ...
        // 创建弱全局引用
        mObject = env->NewWeakGlobalRef(thiz);
    JMediaCodec::~JMediaCodec() {
        // ...
        // 析构函数中,删除弱全局引用
        env->DeleteWeakGlobalRef(mObject);
    void JMediaCodec::handleFrameRenderedNotification(const sp<AMessage> &msg) {
        // ...
        // mOjetct是一个弱全局引用
        // 调用mObject的Java方法
        env->CallVoidMethod(
                mObject, gFields.postEventFromNativeID,
                EVENT_FRAME_RENDERED, arg1, arg2, obj);
    

    从这个例子可以看出,虽然mObject类型虽然为jweak,但是它调用Java方法的方式与jobject并没有区别。

    另外,源码中并没有如下代码来检测这个弱全局指向的Java对象是否被释放了

    jboolean b = env->IsSameObject(mObject, NULL);
    

    最后我们来讨论一个把弱全局引用转化为局部引用的情况。由于弱全局引用无法阻止垃圾回收,为了在JNI函数中安全使用这个弱全局引用,可以把它转化为局部引用,这样就能在函数返回前,暂时阻止垃圾回收,于是可以安全操作底层的Java对象。

    Android源码的一个例子如下

    virtual void onPositionLost(RenderNode& node, const TreeInfo* info) override {
        // 为弱全局引用创建一个局部引用,在这个函数返回前,
        // 可以防止底层Java对象被垃圾回收
        jobject localref = env->NewLocalRef(mWeakRef);
        // ...
        // 局部引用阻止了垃圾回收,因此可以安全调用Java对象的方法
        env->CallVoidMethod(localref, gSurfaceViewPositionLostMethod,
                        info ? info->canvasContext.getFrameNumber() : 0);
        env->DeleteLocalRef(localref);
    

    那么有人可能会问,如果在为弱全局引用创建局部引用时,底层Java对象已经回收了,那怎么办呢?这个只能你用代码来保证,你要保证在Java层对象销毁前,释放底层对象,底层对象再释放弱全局引用。

    在我的工作经验中,几乎没有使用弱全局引用,用的最多的是全局引用。不过如果我们遇到源码中使用弱全局引用的情况,应该多思考为何这样使用,以便在后面的工作使用好弱全局引用。

    docs.oracle.com/javase/7/do…

    docs.oracle.com/javase/7/do…

    分类:
    Android
    标签: