深入浅出Android NDK之全局弱引用的使用

目录
上一篇 深入浅出Android NDK之如何封装一个C++类

新建weaktest.cpp,内容如下:

#include <stdlib.h>
class IShapCallback {
public:
    virtual ~IShapCallback(){};
    virtual void onLoadShap() = 0;
};

class Shap {
public:
    Shap() {
        mCallback = NULL;
    }
    ~Shap() {
        delete mCallback;
    }

    void setCallback(IShapCallback *callback) {
        mCallback = callback;
    }

    void doWork() {
        if (mCallback != NULL) {
            mCallback->onLoadShap();
        }
    }

private:
    IShapCallback *mCallback;
};


#include <jni.h>
#include <android/log.h>
static JavaVM *s_vm;
JNIEnv* getEnv()
{
    JNIEnv *env;
    s_vm->GetEnv((void**)&env,JNI_VERSION_1_4);
    return env;
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    s_vm = vm;
    return JNI_VERSION_1_4;
}

//#define USE_WEAK
class AndroidShapCallback : public IShapCallback {
public:
    AndroidShapCallback(JNIEnv *env, jobject javaCallback) {
        mEnv = env;
#ifdef USE_WEAK
        mJavaCallback = env->NewWeakGlobalRef(javaCallback);
#else
        mJavaCallback = env->NewGlobalRef(javaCallback);
#endif

        mClass = (jclass)env->NewGlobalRef(env->FindClass("com/example/weaktest/Shap$IShapCallback"));
        mMethodID = env->GetMethodID(mClass, "onLoadShap", "()V");
    }

    ~AndroidShapCallback() {
#if 1
        //这么做是正确的,通过JavaVm获得当前线程的JNIEnv
        JNIEnv *env = getEnv();
#else
        /*
         * 这么做是错误的,JNIEnv是跟线程相关的,每个线程都有属于自己的JNIEnv。
         * mEnv是在主线程中传入的,而当前函数是由java的finalize调用的,属于垃圾回收线程,
         * 所以mEnv,不能在当前线程使用。
         */
        JNIEnv *env = mEnv;
#endif
#ifdef USE_WEAK
        env->DeleteWeakGlobalRef(mJavaCallback);
#else
        env->DeleteGlobalRef(mJavaCallback);
#endif
        env->DeleteGlobalRef(mClass);
    }

    virtual void onLoadShap() {
        JNIEnv *env = getEnv();
#ifdef USE_WEAK
        jobject javaCallback = env->NewLocalRef(mJavaCallback);
        if (javaCallback != NULL) {
            env->CallVoidMethod(javaCallback, mMethodID);
            env->DeleteLocalRef(javaCallback);
        } else {
            __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "gloal weak ref IShapCallback already been release");
        }
#else
        env->CallVoidMethod(mJavaCallback, mMethodID);
#endif
    }

private:
    jclass mClass;
    jmethodID mMethodID;
    jobject mJavaCallback;
    JNIEnv *mEnv;
};


extern "C" JNIEXPORT jlong Java_com_example_weaktest_Shap_nativeCreate(JNIEnv *env, jclass clazz) {
    Shap *shap = new Shap();
    return (jlong)shap;
}

extern "C" JNIEXPORT void Java_com_example_weaktest_Shap_nativeRelease(JNIEnv *env, jclass clazz, jlong handle) {
    Shap *shap = (Shap*)handle;
    delete shap;
}


extern "C" JNIEXPORT void Java_com_example_weaktest_Shap_nativeSetCallback(JNIEnv *env, jclass clazz, jlong handle, jobject jcallback) {
    Shap *shap = (Shap*)handle;
    shap->setCallback(new AndroidShapCallback(env, jcallback));
}

extern "C" JNIEXPORT void Java_com_example_weaktest_Shap_nativeDoWork(JNIEnv *env, jclass clazz, jlong handle) {
    Shap *shap = (Shap*)handle;
    shap->doWork();
}

新建Shap.java,内容如下:

package com.example.weaktest;

import android.util.Log;

public class Shap {
    static {
        System.loadLibrary("strtest");
    }
    private long mHandle;
    private  IShapCallback mCallback;

    public interface IShapCallback {
        void onLoadShap();
    }

    public Shap() {
        mHandle = nativeCreate();
    }

    public void setCallback(IShapCallback callback) {
        //mCallback = callback;
        nativeSetCallback(mHandle, callback);
    }

    public void doWork() {
        nativeDoWork(mHandle);
    }

    public void recycle() {
        if (mHandle != 0) {
            Log.i("MD_DEBUG", "shap nativeRelease invoke");
            nativeRelease(mHandle);
            mHandle = 0;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        recycle();
    }

    private static native long nativeCreate();
    private static native void nativeRelease(long handle);
    private static native void nativeSetCallback(long handle, IShapCallback callback);
    private static native void nativeDoWork(long handle);
}

对于以上代码,我们需要注意以下几点:

mClass = (jclass)env->NewGlobalRef(env->FindClass(“com/example/weaktest/Shap$IShapCallback”));

请注意,对于内部类com.example.weaktest.Shap.IShapCallback,调用FindClass时我们传入的是com/example/weaktest/Shap$IShapCallback而不是com/example/weaktest/Shap/IShapCallback

AndroidShapCallback类演示了怎样使用jni,在java层实现C++回调函数。
在实现过程中,最棘手的方式是怎样得到JNIEnv,得到JNIEnv最简单的方式是从jni入口函数获得,因为jni入口函数的第一个参数就是JNIEnv,所以我们最容易想到的办法是将这个JNIEnv存起来,但这样做是错误的。JNIEnv是跟线程相关的,每个线程都有属于自己的JNIEnv,每个线程也只能使用自己的JNIEnv。所以不要试图将jni入口函数的JNIEnv参数保存起来,在以后使用。正确的做法是通过JavaVM的GetEnv得到当前线程的JNIEnv。

#ifdef USE_WEAK
jobject javaCallback = env->NewLocalRef(mJavaCallback);
if (javaCallback != NULL) {
env->CallVoidMethod(javaCallback, mMethodID);
env->DeleteLocalRef(javaCallback);
} else {
__android_log_print(ANDROID_LOG_DEBUG, “MD_DEBUG”, “gloal weak ref IShapCallback already been release”);
}
#else

以上代码演示了如何正确使用全局弱引用,全局弱引用是不能被直接使用的,正确的做法是,先调用NewLocalRef或者NewGlobalRef转换为本地引用或者全局引用再使用。若返回值为NULL,说明全局弱引用所指向的java对象已经被垃圾回收。

//#define USE_WEAK

USE_WEAK宏被注释了,我们先使用全局引用,看看使用全局引用会带来什么问题。
下面我们来测试一下这个例子,新建SecondActivity.java,内容如下:

package com.example.weaktest;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class SecondActivity extends AppCompatActivity {
    Shap mShap;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv = new TextView(this);
        tv.setText("这是第二个activity");
        setContentView(tv);

        mShap = new Shap();
        mShap.setCallback(new Shap.IShapCallback() {
            @Override
            public void onLoadShap() {
                Log.i("MD_DEBUG", "java onLoadShap invoke");
            }
        });
        mShap.doWork();

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mShap.doWork();
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
//        mShap.recycle();
//        或者
//        mShap = null;
    }
}

我们在MainActivity里面直接启动SecondActivity。

package com.example.strtest;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

import com.example.weaktest.SecondActivity;


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //启动第二个activity
        startActivity(new Intent(this, SecondActivity.class));
    }
}

编译运行,界面跳到了SecondActivity,这时候按返回键返回到MainActivity。一切正常看起来好像没什么问题。别着急,打开Profiler,查看一下内存情况,先GC一下,再dump内存,情况如下图所示:
在这里插入图片描述

我们看到SecondActivity发生了泄露。再看下logcat:
在这里插入图片描述
我们在Shap的recycle函数中打印的语句也没有输出。

public void recycle() {
    if (mHandle != 0) {
        Log.i("MD_DEBUG", "shap nativeRelease invoke");
        nativeRelease(mHandle);
        mHandle = 0;
    }
}

为什么会这样呢?SecondActivity为什么没有被释放?下面我们来分析一下:
我们的代码中只有下面这个内部类会引用SecondActivity。

mShap.setCallback(new Shap.IShapCallback() {
@Override
public void onLoadShap() {
Log.i(“MD_DEBUG”, “java onLoadShap invoke”);
}
});

这个实现了IShapCallback的内部类对象现在被native层的mJavaCallback强引用着:

//#define USE_WEAK
class AndroidShapCallback : public IShapCallback {
public:
AndroidShapCallback(JNIEnv *env, jobject javaCallback) {
mEnv = env;
#ifdef USE_WEAK
mJavaCallback = env->NewWeakGlobalRef(javaCallback);
#else
mJavaCallback = env->NewGlobalRef(javaCallback);
#endif

所以问题找到了,因为我们在native层调用了env->NewGlobalRef(javaCallback);将javaCallback强引用了,最终导致了SecondActivity不能释放。
不对,我们在析构函数里明明调用了env->DeleteGlobalRef(mJavaCallback);来释放全局引用呀。
根据刚才的logcat来看析构函数没有被调用过,析构函数为什么没有被调用呢?
我们是在Shap类的finalize中调用的nativeRelease来释放底层对象的。
所以nativeRelease没有被释放是因为Shap对象还被引用着。
Shap对象被谁引用着呢?Shap对象现在被mShap引用着。
mShap是ActivitySecond的成员变量,所以mShap被ActivitySecond引用着。ActivitySecond又被native层引用着,native层又等着mShap释放才能释放。这样就形成了一个循环,谁也释放不了。

怎么解决这个问题呢?造成此问题的根本原因是由于Shap的finalize函数没有被调用,底层对象无法释放导致的。
从这个角度来考虑,有以下解决方案:

  1. 重写ActivtySecond的onDestory方法,将mShap赋值为null。这样Shap对象就会被释放,进而Shap.finalize函数会被调用。
  2. 重写ActivtySecond的onDestory方法,手动调用mShap.recycle()函数来释放底层对象。
  3. mShap不要声明为成员变量,直接使用局部变量。

以上方案能解决问题,但是都不好,会对上层使用者造成困扰。最好的办法就是我们在native层不要全局引用来强引用回调对象,改为全局弱引用。这样ActivitySecond不会被native层强引用,所以不会有泄露。
我们只需要将//#define USE_WEAK的注释取消,一切问题迎刃而解。
但其实还差点儿,我们打开Android Studio的Profiler,手动GC一下,再点击文本框,有如下输出:

2019-10-15 08:34:51.584 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:52.183 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:54.607 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:55.415 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:56.018 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:56.206 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:56.385 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:56.554 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:56.685 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:34:56.864 22815-22815/com.example.strtest I/MD_DEBUG: java onLoadShap invoke
2019-10-15 08:35:21.412 22815-22822/com.example.strtest I/art: Starting a blocking GC Explicit
2019-10-15 08:35:21.445 22815-22822/com.example.strtest I/art: Explicit concurrent mark sweep GC freed 9690(773KB) AllocSpace objects, 3(60KB) LOS objects, 39% free, 2MB/4MB, paused 298us total 32.372ms
2019-10-15 08:35:31.356 22815-22815/com.example.strtest D/MD_DEBUG: gloal weak ref IShapCallback already been release

当GC发生了以后再点击文本,输出了gloal weak ref IShapCallback already been release。这句话是当我们将弱引用转为本地引用失败时打印出来的。

virtual void onLoadShap() {
    JNIEnv *env = getEnv();

#ifdef USE_WEAK
jobject javaCallback = env->NewLocalRef(mJavaCallback);
if (javaCallback != NULL) {
env->CallVoidMethod(javaCallback, mMethodID);
env->DeleteLocalRef(javaCallback);
} else {
__android_log_print(ANDROID_LOG_DEBUG, “MD_DEBUG”, “gloal weak ref IShapCallback already been release”);
}
#else
env->CallVoidMethod(mJavaCallback, mMethodID);
#endif
}

因为我们使用的全局弱引用引用的mJavaCallback,所以当垃圾回收时mJavaCallback对象被回收了,我们下次再调用回调函数的时候就失败了。
怎么办呢?使用全局引用内存泄露,使用全局弱引用,回调有可能不成功。
其实要解决这个问题很简单,我们只需要在java层,把回调对像强引用住就可以了。

public void setCallback(IShapCallback callback) {
    mCallback = callback;
    nativeSetCallback(mHandle, callback);
}

像上面这样,在java层使用强引用,native层使用弱引用,这样才是真的完美。

总结:
当我们在jni层封装类似回调函数这样接口时,使用全局引用会造成内存泄露。这时候我们可以在native层使用全局弱引用,在java层使用强引用的方式来解决该问题。

下一篇 深入浅出Android NDK之在jni中使用线程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值