Android JNI局部引用表溢出——local reference table overflow (max=512)

Home / Android MrLee 2015-4-8 3156


新手在JNI编程中很容易出现这个问题,其原因是局部变量起来了512个超过了JNI设定的数量。
在《JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用》这篇文章中详细介绍了在JNI中三种引用的使用方式,区别、应用场景和开发注意事项。由于都是理论,看完之后可能印象不够深刻,由其是在开发当中容易出错的地方。所以这篇文章用一个例子说明引用使用不当会造成的问题,以引起大家对这个知识点的重视。
首先创建一个Android工程,在主界面放一个文本框和一个按钮,文本框用于接收创建局部引用的数量N,点击按钮后会获取文本框中的数量,然后调用native方法在本地代码中创建一个长度为N的字符串数组,再返回到Java层,并输出到控制台中。

界面如下:

activity_main.xml如下所示:

    
    

在MainActivity中声明native方法和初始化View

package com.example.jni;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends Activity {
    // 返回count个sample相同的字符串数组,并用编号标识,如:sample1,sample2...
    public native String[] getStrings(int count, String sample);
    EditText mEditText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mEditText = (EditText) findViewById(R.id.str_count);
    }
    public void onTestLocalRefOverflow(View view) {
        String[] strings = getStrings(Integer.parseInt(mEditText.getText().toString()),"I Love You %d Year!!!");
        for (String string : strings) {
            System.out.println(string);
        }
    }
    static {
        System.loadLibrary("local_ref_overflow_test");
    }
}

Java中的代码比较简单,MainActivity中声明了一个native方法getStrings,用于调用到本地函数,onTestLocalRefOverflow方法是主界面中按钮的点击事件,点击按钮后调用getStrings方法,传入字符串的数量和字符串内容,然后返回N个相同字符串长度的数组。 接下来,在工程下面创建一个jni目录,并分别创建Android.mk、Application.mk和local_ref_overflow_test.c文件,其中Android.mk是NDK编译系统自动编译和打包C/C++源代码的描述文件。Application.mk用于描述NDK编译时的一些参数选项,如:C/C++预编译宏、CPU架构等。(后续会开文章详细介绍)local_ref_overflow_test.c是实现MainActivity中getStrings本地方法的C代码。 Android.mk文件内容如下所示:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)  #清除环境变量
LOCAL_MODULE    := local_ref_overflow_test #so文件名称,不用加lib前缀和.so后缀
LOCAL_SRC_FILES := local_ref_overflow_test.c #C源文件
LOCAL_LDLIBS    := -llog #链接日志模块
include $(BUILD_SHARED_LIBRARY) #将源文件编译成共享库

Application.mk文件内容如下所示:
APP_ABI     := armeabi armeabi-v7a #指定编译CPU架构类型
local_ref_overflow_test.c文件内容如下所示:
#include 
#include 
#include 
#include 
#include 
#define LOG_TAG "MainActivity"
#define LOG_I(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG, __VA_ARGS__)
#define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#ifdef __cplusplus
extern "C" {
#endif
jobjectArray getStrings(JNIEnv *env, jobject obj, jint count, jstring sample) {
    jobjectArray str_array = NULL;
    jclass cls_string = NULL;
    jmethodID mid_string_init;
    jobject obj_str = NULL;
    const char *c_str_sample = NULL;
    char buff[256];
    int i;
    // 保证至少可以创建3个局部引用(str_array,cls_string,obj_str)
    if ((*env)->EnsureLocalCapacity(env, 3) != JNI_OK) {
        return NULL;
    }
    c_str_sample = (*env)->GetStringUTFChars(env, sample, NULL);
    if (c_str_sample == NULL) {
        return NULL;
    }
    cls_string = (*env)->FindClass(env, "java/lang/String");
    if (cls_string == NULL) {
        return NULL;
    }
    // 获取String的构造方法
    mid_string_init = (*env)->GetMethodID(env, cls_string, "", "()V");
    if (mid_string_init == NULL) {
        (*env)->DeleteLocalRef(env,cls_string);
        return NULL;
    }
    obj_str = (*env)->NewObject(env, cls_string, mid_string_init);
    if (obj_str == NULL) {
        (*env)->DeleteLocalRef(env,cls_string);
        return NULL;
    }
    // 创建一个字符串数组
    str_array = (*env)->NewObjectArray(env, count, cls_string, obj_str);
    if (str_array == NULL) {
         (*env)->DeleteLocalRef(env,cls_string);
         (*env)->DeleteLocalRef(env,obj_str);
        return NULL;
    }
    // 给数组中每个元素赋值
    for (i = 0; i < count; ++i) {
        memset(buff, 0, sizeof(buff));   // 初始一下缓冲区
        sprintf(buff, c_str_sample,i);
        jstring newStr = (*env)->NewStringUTF(env, buff);
        (*env)->SetObjectArrayElement(env, str_array, i, newStr);
    }
    // 释放模板字符串所占的内存
    (*env)->ReleaseStringUTFChars(env, sample, c_str_sample);
    // 释放局部引用所占用的资源
    (*env)->DeleteLocalRef(env, cls_string);
    (*env)->DeleteLocalRef(env, obj_str);
    return str_array;
}
const JNINativeMethod g_methods[] = {
        {"getStrings", "(ILjava/lang/String;)[Ljava/lang/String;", (void*)getStrings}
};
static jclass g_cls_MainActivity = NULL;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    LOG_I("JNI_OnLoad method call begin");
    JNIEnv* env = NULL;
    jclass cls = NULL;
    if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    // 查找要加载的本地方法Class引用
    cls = (*env)->FindClass(env, "com/example/jni/MainActivity");
    if(cls == NULL) {
        return JNI_ERR;
    }
    // 将class的引用缓存到全局变量中
    g_cls_MainActivity = (*env)->NewWeakGlobalRef(env, cls);
    (*env)->DeleteLocalRef(env, cls);   // 手动删除局部引用是个好习惯
    // 将java中的native方法与本地函数绑定
    (*env)->RegisterNatives(env, g_cls_MainActivity, g_methods, sizeof(g_methods) / sizeof(g_methods[0]));
    LOG_I("JNI_OnLoad method call end");
    return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved)
{
    LOG_I("JNI_OnUnload method call begin");
    JNIEnv *env = NULL;
    if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
        return;
    }
    (*env)->UnregisterNatives(env, g_cls_MainActivity); // so被卸载的时候解除注册
    (*env)->DeleteWeakGlobalRef(env, g_cls_MainActivity);
}
#ifdef __cplusplus
}
#endif

如果你是从之前的文章阅读过来的,上述本地代码有几个函数可能是没见过的。下面简单说明一下,后面会写文章详细介绍。其中JNI_OnLoad是在Java层调用System.loadLibrary方法加载共享库到虚拟机时的回调函数,在这里适合做一些初始化处理。JNI_OnUnload函数是在共享库被卸载的时候由虚拟机回调,适合做资源释放与内存回收的处理。第104行的RegisterNatives函数用于将本地函数与Java的native方法进行绑定。在本例中,没有按原来的方式用javah命令生成头文件的声明,而是用RegisterNatives函数将Java中的getStrings native方法与本地函数getStrings绑定在了一起。同样能实现函数查找的功能,而且效率更高。JNINativeMethod是一个数据结构,用于描述一个方法名称、函数签名和函数指针信息,用于绑定本地函数与Java native方法的映射关系。如下所示:
typedef struct {
    char *name;      // 函数名称
    char *signature; // 函数签名
    void *fnPtr;     // 函数指针
} JNINativeMethod;

注意:void *fnPtr这个函数指针所指向的函数参数要注意,本地函数的第一个参数必须是JNIEnv*,第二个参数如果是实例方法则是jobject,静态方法则是jclass,后面的才是Java中native方法的参数。例如上例中MainActivity中声明的native方法getStrings:public native String[] getStrings(int count, String sample); 对应本地函数 jobjectArray getStrings(JNIEnv *env, jobject obj, jint count, jstring sample)。
getStrings的代码我就不详细介绍了,就是创建一个字符串数组的功能,之前的文章已经讲过很多次了。现在仔细阅读下这个函数的实现,看能不能找出哪个地会造成局部引用表溢出。如果现在就运行程序,并在文本框中输入大于501以上的值的话,就会看到因局部引用表溢出而崩溃的现象。如下图所示:

这时你可能会想到利用上篇文章学到的EnsureLocalCapacity或PushLocalFrame/PopLocalFrame接口来扩充局部引用的数量。例如,将第25行改成if ((*env)->EnsureLocalCapacity(env, count + 3) != JNI_OK),保证在函数中可以创建count+3个数量的引用(这里的3是指str_array、cls_string和obj_str)。不过遗憾的是,EnsureLocalCapacity会试图申请指定数量的局部引用,但不一定会申请成功,因为局部引用是创建在栈中的,如果这个数量级的引用所申请的内存空间超出了栈的最大内存空间范围,就会造成内存溢出。结果如下图所示:


所以在一个本地方法中,如果使用了大量的局部引用而没有及时释放的话,随时都有可能造成程序崩溃的现象。在上例中63行处,每遍历一次,都会创建一个新的字符串并返回指向这个字符串的局部引用,而在64行使用完之后,就没有管它了,从而造成创建较大数组的情况下,就会把局部引用表填满,造成引用表溢出。经测试,在Android中局部引用表默认最大容量是512个。这是虚拟机实现的,在程序中应该没办法修改这个数量。看到这,我想你应该知道怎么修正这个问题了吧。是的,直接在64行将字符串设置到数组元素中后,调用DeleteLocalRef删除即可。修改后的代码如下所示:
// 给数组中每个元素赋值
for (i = 0; i < count; ++i) {
    memset(buff, 0, sizeof(buff));   // 初始一下缓冲区
    sprintf(buff, c_str_sample,i);
    jstring newStr = (*env)->NewStringUTF(env, buff);
    (*env)->SetObjectArrayElement(env, str_array, i, newStr);
    (*env)->DeleteLocalRef(env,newStr);   // Warning: 这里如果不手动释放局部引用,很有可能造成局部引用表溢出
}
修改完之后,你创建多大的字符串数组都没有问题了。当然不能超过物理内存的大小啦!因为Java中的创建的对象所分配的内存全都存储在堆空间。下面创建50万个长度的字符串数组来验证下,如下图所示:


Demo GIT下载地址:git@code.csdn.net:xyang81/jnilocalrefoverflowtest.git

本文链接:https://www.it72.com/2139.htm

推荐阅读
最新回复 (0)
返回