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

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44873769

《JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用》这篇文章中详细介绍了在JNI中三种引用的使用方式,区别、应用场景和开发注意事项。由于都是理论,看完之后可能印象不够深刻,由其是在开发当中容易出错的地方。所以这篇文章用一个例子说明引用使用不当会造成的问题,以引起大家对这个知识点的重视。

首先创建一个Android工程,在主界面放一个文本框和一个按钮,文本框用于接收创建局部引用的数量N,点击按钮后会获取文本框中的数量,然后调用native方法在本地代码中创建一个长度为N的字符串数组,再返回到Java层,并输出到控制台中。

界面如下:


activity_main.xml如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:padding="5dip" >

    <EditText
        android:id="@+id/str_count"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:inputType="numberDecimal" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/str_count"
        android:onClick="onTestLocalRefOverflow"
        android:text="局部引用表溢出测试" />

</LinearLayout>

在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 <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <android/log.h>

#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, "<init>", "()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以上的值的话,就会看到因局部引用表溢出而崩溃的现象。如下图所示:

这时你可能会想到利用上篇文章学到的EnsureLocalCapacityPushLocalFrame/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

时间: 2024-10-17 09:24:16

Android JNI局部引用表溢出:local reference table overflow (max=512)的相关文章

JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44657385     这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项.可能看起来有点枯燥,但引用是在JNI中最容易出错的一个点,如果使用不当,容易使程序造成内存溢出,程序崩溃等现象.所以讲得比较细,有些地方看起来可能比较啰嗦,还请轻啪!<Android JNI局部引用表溢出:local reference table overflow (max=512)>这

android JNI调用机制

JNI的出现使得开发者既可以利用Java语言跨平台.类库丰 富.开发便捷等特点,又可以利用Native语言的高效. JNI是JVM实现中的一部分,因此Native语言和Java代码都运行在JVM的宿主环境. JNI是一个双向的接口:开发者不仅可以通过JNI在Java代码中访问Native模块,还可以在 Native代码中嵌入一个JVM,并通过JNI访问运行于其中的Java模块.可见,JNI担任了一个桥梁的角色,它将JVM与Native模块联系起 来,从而实现了Java代码与Native代码的互访

android JNI学习

经过几天的努力终于搞定了android JNI部分,下面将我的这个小程序和大家分享一下.android JNI是连接android Java部分和C/C++部分的纽带,完整使用JNI需要Java代码和C/C++代码.其中C/C++代码用于生成库文件,Java代码用于引用C /C++库文件以及调用C/C++方法. android Java部分代码: 01 jnitest.java 02   03 package com.hello.jnitest; 04   05    06   07 impor

Android加载图片内存溢出问题解决方法

  这篇文章主要介绍了Android加载图片内存溢出问题解决方法,本文讲解使用BitmapFactory.Options解决内存溢出问题,需要的朋友可以参考下 1. 在Android软件开发过程中,图片处理是经常遇到的. 在将图片转换成Bitmap的时候,由于图片的大小不一样,当遇到很大的图片的时候会出现超出内存的问题,为了解决这个问题Android API提供了BitmapFactory.Options这个类. 2. 由于Android对图片使用内存有限制,若是加载几兆的大图片便内存溢出.Bi

android JNI调用SDK底层C方法

问题描述 android JNI调用SDK底层C方法 如题:因为上层没法去找到方法,只有想办法去调底层C的方法,SDK源码make出来的.so库文件可以用来做jni的调用么? 底层.c文件里面没有申明JNI,我需要如何去申明 解决方案 你这几个函数的参数,返回值都比较简单,按照JNI的规则,封装导出一下,然后就可以java来调用了 http://www.cnblogs.com/anyanran/archive/2010/11/22/ndk1.html 解决方案二: http://blog.csd

文件读取-Android jni c++ 如何读取jni文件夹下的文件?

问题描述 Android jni c++ 如何读取jni文件夹下的文件? 在一个jni工程中,jni文件夹的结构如下: jni |--Android.mk |--Application.mk |--filer.h |--file.cpp |--res.txt file.cpp里边的代码如下: #include ""filer.h""#include <fstream>#include <string>using namespace std;j

android 布局-Android自定义控件,引用里面的方法,如何引用

问题描述 Android自定义控件,引用里面的方法,如何引用 我自己定义了一个继承自gridview的类,主布局引用,想在主布局里再添加一个button,然后这个button点击处理事件是自定义控件里的一个方法,该怎么调用? new mygridview().方法:不能实现,点击按钮后,程序崩溃 解决方案 你自定义的控件如果是在xml文件中添加到主布局,需要通过findViewById()获取控件的实例,然后调用其方法. 自定义如果是mygridview view=new mygridview(

Android JNI的实例代码流程图

0Android JNI实例代码总体流程图 1C代码部分 2C代码部分 3Java代码部分 0.Android JNI实例代码总体流程图 1.C代码部分 2.C++代码部分 3.Java代码部分 Wu_Being博客声明:本人博客欢迎转载,请标明博客原文和原链接!谢谢! <Android JNI的实例代码流程图>: http://blog.csdn.net/u014134180/article/details/78125723 如果你看完这篇博文,觉得对你有帮助,并且愿意付赞助费,那么我会更有

android jni传递结构体类型给c函数,返回结构体,怎么写啊

问题描述 android jni传递结构体类型给c函数,返回结构体,怎么写啊 android jni传递结构体类型给c函数,返回结构体,怎么写啊 c方法:OutAnalyseResultPacket* EcgMainPro(InEcgPacket* pPacKet) 参数和返回值都是结构体类型 解决方案 参考:http://blog.sina.com.cn/s/blog_414e587f0101411f.html 另外,C那里返回结构体指针不太好,因为你没有机制去释放这些内存.容易内存泄漏.除非