在[《JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用》](http://blog.csdn.net/xyang81/article/details/44657385)這篇文章中詳細介紹了在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以上的值的話,就會看到因局部引用表溢出而崩潰的現象。如下圖所示:

這時你可能會想到利用上篇文章學到的`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
- 前言
- JNI/NDK開發指南(開山篇)
- JNI/NDK開發指南(一)—— JNI開發流程及HelloWorld
- JNI/NDK開發指南(二)——JVM查找java native方法的規則
- JNI/NDK開發指南(三)——JNI數據類型及與Java數據類型的映射關系
- JNI/NDK開發指南(四)——字符串處理
- Android NDK開發Crash錯誤定位
- JNI/NDK開發指南(五)——訪問數組(基本類型數組與對象數組)
- JNI/NDK開發指南(六)——C/C++訪問Java實例方法和靜態方法
- JNI/NDK開發指南(七)——C/C++訪問Java實例變量和靜態變量
- JNI/NDK開發指南(八)——調用構造方法和父類實例方法
- JNI/NDK開發指南(九)——JNI調用性能測試及優化
- JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用
- Android JNI局部引用表溢出:local reference table overflow (max=512)
- JNI/NDK開發指南(十一)——JNI異常處理