目录
上一篇 深入浅出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函数没有被调用,底层对象无法释放导致的。
从这个角度来考虑,有以下解决方案:
- 重写ActivtySecond的onDestory方法,将mShap赋值为null。这样Shap对象就会被释放,进而Shap.finalize函数会被调用。
- 重写ActivtySecond的onDestory方法,手动调用mShap.recycle()函数来释放底层对象。
- 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层使用强引用的方式来解决该问题。