深入浅出Android NDK之jni三大引用(本地引用、全局引用、全局弱引用)

目录
上一篇 深入浅出Android NDK之异常处理

在java中有强引用,软引用,弱引用,虚引用四种引用类型。
在jni中有全局引用,全局弱引用,本地引用三种引用类型。
在C中有全局变量,局部变量,全局变量两种变量类型。
初学者经常将这几者弄混。
我们知道在java中强引用引用的对象是不会被垃圾回收的。软引用引用的对象在内存不足时会被回收。弱引用引用的对象只要垃圾回收发生就会被回收。
从java的角度来看,jni中的全局引用和本地引用都是java中的强引用,jni中的全局弱引用是java中的弱引用。
在jni中,本地引用一般是自动创建和自动释放的。
而全局引用和全局弱引用则需要我们手动创建和手动释放。
请看以下例子:

extern "C" JNIEXPORT Java_com_example_Test_test(JNIEnv *env, jclass clazz,jobject jobj) {
	//参数clazz,jobj是本地引用
	
	jclass cls = env->FindClass("android/graphics/PointF");//cls是一个本地引用
	jmehtod mid = env->GetMethodID("<init>", "()V");
	jobject lobj= env->NewObject(cls, mid);//localobj是一个本地引用
	jobject gobj = env->NewGlobalRef(obj);//创建了一个全局引用gobj
	jobject wobj = env->NewWeakGlobalRef(obj);//创建了一个全局弱引用wobj
	...
	...
	...
	env->DeleteGlobalRef(gobj);//全局引用需要手动释放
	env->DeleteWeakGlobalRef(wobj);//全局弱引用需要手动释放
}//方法返回后本地引用clazz,jobj,cls,lobj会自动释放

我们知道JNI中基本数据类型有:

jboolean jbyte jchar jshort jint jlong jfloat jdouble

对象数据类型有:

jobject jstring jclass jxxxArray jthrowable

请牢牢记住,JNIEnv所提供的函数中,除了NewGlobalRef和NewWeakGlobalRef以外,所有返回值是对象类型的都是本地引用。
jni入口参数中所有对象类型的变量也都是本地引用,比如Java_com_example_Test_test中的jclass clazz和jobject jobj。
所以,以下函数都会创建一个本地引用并返回这个本地引用:


jclass DefineClass(const char *name, jobject loader, const jbyte* buf,jsize bufLen)
jclass FindClass(const char* name)
jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic)
jclass GetSuperclass(jclass clazz)
jobject ToReflectedField(jclass cls, jfieldID fieldID, jboolean isStatic)
jthrowable ExceptionOccurred()
jobject PopLocalFrame(jobject result)
jobject NewLocalRef(jobject ref) 
jobject AllocObject(jclass clazz)
jobject NewObject(jclass clazz, jmethodID methodID, ...)
jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)
jobject NewObjectA(jclass clazz, jmethodID methodID, const jvalue* args)
jclass GetObjectClass(jobject obj)
jobject CallObjectMethod(jobject obj, jmethodID methodID, ...) 
jobject CallObjectMethodV(jobject obj, jmethodID methodID,va_list args) 
jobject CallObjectMethodA(jobject obj, jmethodID methodID, const jvalue* args)   
jobject CallNonVirtualObjectMethod(jobject obj, jmethodID methodID, ...) 
jobject CallNonVirtualObjectMethodV(jobject obj, jmethodID methodID,va_list args) 
jobject CallNonVirtualObjectMethodA(jobject obj, jmethodID methodID, const jvalue* args)   
jobject CallStaticObjectMethod(jobject obj, jmethodID methodID, ...) 
jobject CallStaticObjectMethodV(jobject obj, jmethodID methodID,va_list args) 
jobject CallStaticObjectMethodA(jobject obj, jmethodID methodID, const jvalue* args)   
jobject GetObjectField(jobject obj, jfieldID fieldID)
jobject GetStaticObjectField(jclass clazz, jfieldID fieldID)
jstring NewString(const jchar* unicodeChars, jsize len)
jstring NewStringUTF(const char* bytes)
jobjectArray NewObjectArray(jsize length, jclass elementClass,
jobject GetObjectArrayElement(jobjectArray array, jsize index)
jXXXArray NewXXXArray(jsize length)
jobject NewDirectByteBuffer(void* address, jlong capacity)

本地引用为什么能够自动创建和释放呢?
当我们调用java的native函数的时候,虚拟机首先会创建一个本地引用表,然后再调用C层相应的入口函数,在C层函数执行期间,所有C层创建的类型对象的引用都会被加入本地引用表。当C层函数执行完毕,返回到java层时,本地引用表中的所有引用会被一次性释放。
注意,本地引用表的大小是有限制的,在Android上,本地引用表的大小是512,所以在一次jni函数的调用过程中,C层最多可以创建512个本地引用,当超过这个数量时,会报下面这个错误:

JNI ERROR (app bug): local reference table overflow (max=512)

所以,从编程的角度来看,本地引用表其实就是就是一个大小固定的数组。

下面我们来看一个例子,RefTest.java的内容如下:

package com.example;

public class RefTest {
    static  {
        System.loadLibrary("strtest");
    }

    public static native void testOverflow();
}

RefTest.cpp的内容如下:

#include <jni.h>
#include <android/log.h>

extern "C" JNIEXPORT void Java_com_example_RefTest_testOverflow(JNIEnv *env, jclass clazz) {
    jmethodID mid = env->GetMethodID(clazz, "<init>", "()V");
    for (int i = 0; i < 1000; i++) {
        __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "env->NewObject %d", i);
        jobject obj = env->NewObject(clazz, mid);
//        jclass cls = env->FindClass("java/lang/String");
    }
}

编译运行,结果如下:

D/MD_DEBUG: env->NewObject 0
D/MD_DEBUG: env->NewObject 1
.
.
.
D/MD_DEBUG: env->NewObject 503
D/MD_DEBUG: env->NewObject 504
D/MD_DEBUG: env->NewObject 505
D/MD_DEBUG: env->NewObject 506
A/art: art/runtime/indirect_reference_table.cc:116] JNI ERROR (app bug): local reference table overflow (max=512)
A/art: art/runtime/indirect_reference_table.cc:116] local reference table dump:
A/art: art/runtime/indirect_reference_table.cc:116]   Last 10 entries (of 512):
A/art: art/runtime/indirect_reference_table.cc:116]       511: 0x12d64ce8 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       510: 0x12d64ce0 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       509: 0x12d64cd8 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       508: 0x12d64cd0 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       507: 0x12d64cc8 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       506: 0x12d64cc0 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       505: 0x12d64cb8 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       504: 0x12d64cb0 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       503: 0x12d64ca8 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]       502: 0x12d64ca0 com.example.RefTest
A/art: art/runtime/indirect_reference_table.cc:116]   Summary:
A/art: art/runtime/indirect_reference_table.cc:116]       506 of com.example.RefTest (506 unique instances)
A/art: art/runtime/indirect_reference_table.cc:116]         2 of java.lang.Class (2 unique instances)
A/art: art/runtime/indirect_reference_table.cc:116]         3 of java.lang.String (3 unique instances)
A/art: art/runtime/indirect_reference_table.cc:116]         1 of java.lang.String[] (3 elements)
A/art: art/runtime/indirect_reference_table.cc:116] 
A/art: art/runtime/runtime.cc:403] Runtime aborting...
A/art: art/runtime/runtime.cc:403] Aborting thread:
A/art: art/runtime/runtime.cc:403] "main" prio=5 tid=1 Runnable
A/art: art/runtime/runtime.cc:403]   | group="" sCount=0 dsCount=0 obj=0x7570e4e8 self=0xe6904400

在代码中我们执行了1000次循环,在第506次调用env->NewObject时,发生了崩溃。我们分析一下崩溃信息:

local reference table overflow (max=512)
JNI ERROR (app bug): local reference table overflow (max=512)
...
Summary:
    506 of com.example.RefTest (506 unique instances)
      2 of java.lang.Class (2 unique instances)
	  3 of java.lang.String (3 unique instances)
	  1 of java.lang.String[] (3 elements)

大概意思是说,本地引用表溢出了,最大是512个,现在的512个里面有506个com.example.RefTest,2个java.lang.Class,3个java.lang.String,1个java.lang.String[],加起来刚好512个。
其中占大头的那506个com.example.RefTest确实是我们刚刚调用env->NewObject分配出来的。
也就是说我们刚刚调用env->NewObject创建的com.example.RefTest对象都被加入了一个叫做本地引用表的表中,这个表呢最大容量是512,并且里面已经有6个了,所以我们最多只能调用506次env->NewObject。
如果你不服气,你可以再做个试验,修改RefTest.cpp的代码如下:

extern "C" JNIEXPORT void Java_com_example_RefTest_testOverflow(JNIEnv *env, jclass clazz) {
    //jmethodID mid = env->GetMethodID(clazz, "<init>", "()V");
    for (int i = 0; i < 1000; i++) {
        __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "env->FindClass %d", i);
        //jobject obj = env->NewObject(clazz, mid);
        jclass cls = env->FindClass("java/lang/String");
    }
}

循环调用1000次env->FindClass函数,结果是一样的,依然会在第506次循环处崩溃。
对于绝大多数刚刚学会了怎么在C代码中创建java对像的人来说,基本上都会遇到这个问题。
当遇到以上问题时,我们便不能再依赖于本地引用的自动创建释放机制了,应该调用DeleteLocalRef手动释放本地引用,参考以下代码:

extern "C" JNIEXPORT void Java_com_example_RefTest_testOverflow(JNIEnv *env, jclass clazz) {
    //jmethodID mid = env->GetMethodID(clazz, "<init>", "()V");
    for (int i = 0; i < 1000; i++) {
        __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "env->FindClass %d", i);
        //jobject obj = env->NewObject(clazz, mid);
        jclass cls = env->FindClass("java/lang/String");
        env->DeleteLocalRef(cls);//手动释放本地引用
        //请注意,因为我们已经调用了DeleteLocalRef将cls释放了,所以后续代码不应该再对cls进行任何操作
    }
}

在实际应用了除了一个个调用DeleteLocalRef释放本地引用外,我们还有一种更简单的方式来批量释放本地引用,参考以下代码:

extern "C" JNIEXPORT void Java_com_example_RefTest_drawText(JNIEnv *env, jclass clazz, jobject canvas) {
    const char *text[] = {"第一行","第二行","第三行","第四行","第五行","第六行","第七行","第八行","第九行","第十行"};
    env->PushLocalFrame(10);//Frame1
    jclass clsCanvas = env->FindClass("android/graphics/Canvas");// in frame1
    jmethodID midDrawText = env->GetMethodID(clsCanvas, "drawText", "(Ljava/lang/String;FFLandroid/graphics/Paint;)V");
    jclass clsPaint = env->FindClass("android/graphics/Paint");// in frame1
    jmethodID  midPaintInit = env->GetMethodID(clsPaint, "<init>","()V");
    jobject jpaint = env->NewObject(clsPaint, midPaintInit);// in frame1
    for (int i = 0; i < 10; i++) {
        env->PushLocalFrame(2);// Frame2
        jstring str = env->NewStringUTF(text[i]); // in frame2
        env->CallVoidMethod(canvas, midDrawText, str, (jfloat)0, (jfloat)(i * 30), jpaint);//canvas.drawText(str, (jfloat)0, (jfloat)(i * 30), jpaint);
        env->PopLocalFrame(NULL);//批量释放,相当于env->DeleteLocalRef(str);并返回到frame1
    }
    //env->NewObject()//如果有的话,in frame1
    env->PopLocalFrame(NULL);//批量释放,相当于env->DeleteLocalRef(clsCanvas);env->DeleteLocalRef(clsPaint);env->DeleteLocalRef(jpaint);
}

假设本地引用表已经使了10个,那么还剩下502个可用,当我们调用env->PushLocalFrame(10)的时候,相当于在剩下的502个中分配出10个的空间,所以本地引用表还剩492(512-10-10)个空间。我们给这个空间起个名字叫frame1。现在frame1位于栈顶,所以在之后的代码创建的本地引用都会放在frame1中。当我们调用env->PushLocalFrame(2)的时候,在剩余的492个空间中分配出2个空间,本地引用表还剩490个空间。现在frame2位于栈顶,所以之后的代码创建的本地引用都会放在frame2中,当我们调用env->PopLocalFrame(NULL)后,frame2出栈,此时frame1又重新位于栈顶,所以创建的本地引用会重新放在放在frame1中。当再次调用env->PopLocalFrame(NULL),frame1出栈,本地引用表中没有任何frame了,本地引用会直接放在本地引用表中。
在这里插入图片描述
如果PushLocalFrame(10),那么我们最多只能分配10个本地引用,如果超过10个,程序会崩溃。
使用PushLocalFrame/PopLocalFrame有以下好处:
1.代码更清晰,不会漏掉DeleteLocalRef
2.不需要再辛苦的区分JNIEnv的函数返回的是不是本地引用,需不需要被Delete了。比如jmethodID,jfieldID是不是本地引用?需不需要Delete?不要想了,直接把代码放在PushLocalFrame和PopLocalFrame之间就可以了。
3.释放效率更高,这一点儿是我自己的看法。

在调用env->PopLocalFrame的时候我们传入了NULL,这个参数有什么意义,请看下面这个例子:

jobject createPoint(JNIEnv *env,float x, float y) {
    env->PushLocalFrame(5);
    jclass cls = env->FindClass("android/graphics/PointF");
    jmethodID mid = env->GetMethodID(cls, "<init>", "(FF)V");
    jobject jpoint = env->NewObject(cls, mid, x, y);

    //cls会被释放,但是jpoint不会,jpoint会放入上一个frame
    env->PopLocalFrame(jpoint);
    return jpoint;
}

extern "C" JNIEXPORT void Java_com_example_RefTest_test1(JNIEnv *env, jclass clazz) {
    jclass cls = env->FindClass("android/graphics/PointF");
    jobjectArray jarray = env->NewObjectArray(4, cls, NULL);
   for (int i = 0; i < 1000; i++) {
       jobject jpoint = createPoint(env,4 * i, 4 * i);
       env->SetObjectArrayElement(jarray, i, jpoint);
       env->DeleteLocalRef(jpoint);
   }
}

当我们需要将一个对象类型的变量做为返回值返回时,可以将其做为参数传入env->PopLocalFrame,这样PopLocalFrame不会释放这个本地引用,而是将其放入上一个frame,若没有上一个frame,则放入本地引用表,这样,我们才能在后续的代码继续使用它。

本地引用我们就讲完了,那什么时候我们需要使用全局引用呢?下面我们看一个例子:

#include <vector>
#include <android/log.h>
std::vector<jobject> gVecText;//全局变量
extern "C" JNIEXPORT void Java_com_example_RefTest_addText(JNIEnv *env, jclass clazz, jstring ltext) {
#if 0
    //这么做是错误的,因为ltext是本地引用,Java_com_example_RefTest_addText返回后ltext就被释放了。
    gVecText.push_back(ltext);
#else
    jobject gtext = env->NewGlobalRef(ltext);//创建一个全局引用,将全局引用加入
    gVecText.push_back(gtext);
#endif
}
extern "C" JNIEXPORT void Java_com_example_RefTest_addTextBatch(JNIEnv *env, jclass clazz, jobjectArray textArray) {
    jint len = env->GetArrayLength(textArray);
    for (int i = 0; i < len; i++) {
#if 0
        jobject ltext = env->GetObjectArrayElement(textArray, i);
        gVecText.push_back(env->NewGlobalRef(ltext));

        //一定要释放,因为env->GetObjectArrayElement会创建一个本地引用。
        // 如果len比较大,有可能会引起本地引用表不够用而产生崩溃。
        env->DeleteLocalRef(ltext);
#else
        //也可以这样
        env->PushLocalFrame(5);
        gVecText.push_back(env->NewGlobalRef(env->GetObjectArrayElement(textArray, i)));
        env->PopLocalFrame(NULL);
#endif
    }
}

extern "C" JNIEXPORT void Java_com_example_RefTest_printTextTable(JNIEnv *env, jclass clazz) {
    //gVecText里面装的都是全局引用,所以可以在这里使用。
    for (size_t i = 0; i < gVecText.size(); i++) {
        const char * ctext = env->GetStringUTFChars((jstring)gVecText[i], NULL);
        __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "text %d is:", i, ctext);
        env->ReleaseStringUTFChars((jstring)gVecText[i], ctext);
    }
}

一般情况下,使用一个全局变量来存储一个本地引用都是错误的做法,因为本地引用在jni调用完成之后就会被释放,所以在这之后的其他函数中再调用已经被释放的本地引用会引起程序崩溃。
所以当本地引用需要跨jni函数使用的时候,我们需要将其转为全局引用或者全局弱引用来存储。
关于全局弱引用的用法之后我会专门用一章来讲解,在jni中,全局弱引用一般用于解决由全局引用引起的循环引用而造成的内存泄露问题。

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值