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层对象销毁前,释放底层对象,底层对象再释放弱全局引用。
工作感想
在我的工作经验中,几乎没有使用弱全局引用,用的最多的是全局引用。不过如果我们遇到源码中使用弱全局引用的情况,应该多思考为何这样使用,以便在后面的工作使用好弱全局引用。