人间观察
好像什么都来得及,又好像什么都来不及。
本篇文章主要介绍在jni开发中常见的三种引用的使用方法和注意事项以及jni和java交互的缓存策略。
我们知道Java是一门纯面象对象的语言,除了基本数据类型外,其它任何类型所创建的对象的内存都存在堆空间中。内存由JVM 的GC(Garbage Collection)垃圾回收进行管理。
但是对于c,c++中以及用c/c++编写的jni来说同样需要手动管理和处理内存,特别是引用类型的对象。malloc,realloc,free ,delete
,不像java有jvm对每个进程内存的限制,特别是Android移动端使用不当给你oom好不啦。而在c++/c只要你想要多大的来随便搞(只要系统内存充足)但是需要释放。各有各的优势。
在jni中分为局部引用,全局引用,全局弱引用,个人认为有点类似于java中
局部引用,强引用,软引用SoftReference
。在使用介绍之前我们先看一下jni中的基本类型和引用类型有哪些以及对应关系。
jni数据类型
基本数据类型:
java与Native映射关系如下表所示:
Java类型 | Native 类型 | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint signed | 32 bits |
long jlong | signed | 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | not applicable |
引用数据类型
外面的为jni中的,括号中的java中的。
- jobject
- jclass (java.lang.Class objects)
- jstring (java.lang.String objects)
- jarray (arrays)
- jobjectArray (object arrays)
- jbooleanArray (boolean arrays)
- jbyteArray (byte arrays)
- jcharArray (char arrays)
- jshortArray (short arrays)
- jintArray (int arrays)
- jlongArray (long arrays)
- jfloatArray (float arrays)
- jdoubleArray (double arrays)
- jthrowable (java.lang.Throwable objects)
上面的层次中的jni的引用类型代表了继承关系,jbooleanArray继承jarray,jarray集成jobject,最终都集成jobject。
局部引用
通过调用jni的一些方法比如FindClass
,NewCharArray
,NewStringUTF
等只要是返回上面介绍的jni的引用类型都属于局部引用,局部引用的生命周期只在方法中效,不能垮线程跨方法使用,函数退出后局部引用所引用的对象会被JVM自动释放,或显示调用DeleteLocalRef
释放。局部引用的也可以通过(*env)->NewLocalRef(env,local_ref)
方法创建,一般不常用。
如下示例:
// jni_ref.cpp
// 在jni中调用java String类构造返回String
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jnilocalRef(JNIEnv *env, jobject instance) {
// 局部引用
jclass local_j_cls = env->FindClass("java/lang/String");
// 调用public String(char[] value); 构造方法。 为了演示更多的局部引用
jmethodID j_mid = env->GetMethodID(local_j_cls, "<init>", "([C)V");
// 局部引用
jcharArray local_j_charArr = env->NewCharArray(8);
// 局部引用
jstring local_str = env->NewStringUTF("LocalRef");
const jchar *j_char = env->GetStringChars(local_str, nullptr);
env->SetCharArrayRegion(local_j_charArr, 0, 8, j_char);
jstring j_str = (jstring) env->NewObject(local_j_cls, j_mid, local_j_charArr);
// 释放局部引用,也可以不用调用在方法结束后jvm会自动回收,最好有良好的编码习惯
env->DeleteLocalRef(local_j_cls);
env->DeleteLocalRef(local_str);
env->DeleteLocalRef(local_j_charArr);
// 也可以通过NewLocalRef函数创建 (*env)->NewLocalRef(env,local_ref);这个方法一般很少用。
// 函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放。(*env)->DeleteLocalRef(env,local_ref)
// ReleaseStringChars和GetStringChars对应
env->ReleaseStringChars(j_str, j_char);
return j_str;
}
例子中的local_j_cls,local_j_charArr,local_j_charArr,j_str 都是局部引用类型。最后调用了DeleteLocalRef
来释放。
有同学问了,既然局部引用不用手动释放,可不可以不用调用DeleteLocalRef
方法。
咦,你这个小可爱,好问题哦!
我网上搜索了下,大部分的文章说了下会有限制。超过512个局部引用(为什么是这个数字,一看就是一个有情怀的程序员)会造成局部引用表溢出。我还是想测试一下如下
// jni_ref.cpp
LOG_D("localRefOverflow start");
for (int i = 0; i < count; i++) {
jclass local_j_cls = env->FindClass("java/util/ArrayList");
// env->DeleteLocalRef(local_j_cls);
}
LOG_D("localRefOverflow end");
count =513,没有报错,打印了localRefOverflow end
count =2000,没有报错,打印了localRefOverflow end
count =10000,没有报错,打印了localRefOverflow end
count =10 0000,没有报错,打印了localRefOverflow end
count =100 0000,没有报错,打印了localRefOverflow end
…
我靠,WTF? 直接for循环900w次。异常出现了。
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] JNI ERROR (app bug): local reference table overflow (max=8388608)
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] local reference table dump:
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] Last 10 entries (of 8388608):
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388607: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388606: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388605: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388604: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.476 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388603: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388602: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388601: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388600: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388599: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388598: 0x706ca3a0 java.lang.Class<java.util.ArrayList>
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] Summary:
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 8388604 of java.lang.Class (3 unique instances)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 3 of java.lang.String (3 unique instances)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] 1 of java.lang.String[] (3 elements)
2020-10-16 18:05:11.477 26699-26699/com.bj.gxz.jniapp A/zygote: indirect_reference_table.cc:273] Resizing failed: Requested size exceeds maximum: 16777216
8388608 ,可以猜测是不同的Android 版本导致,Android经常这样不同的API或者功能在不同的版本上表现不一样。 而我我用的Android 8.1的系统。为什么512没有报错了。
Android 8.0 之前局部引用表的上限是512个引用,Android 8.0后局部引用表上限提升到了8388608个引用。大家想一探究竟的话可以在如下Android 底层代码中看一下。
有关底层源码
看源码的同时我们也看到了比如FindClass
等方法,在最后方法的最后都有类似添加到局部引用表里的代码,也就是说需要我们手动删除局部引用。
static jclass FindClass(JNIEnv* env, const char* name) {
CHECK_NON_NULL_ARGUMENT(name);
Runtime* runtime = Runtime::Current();
ClassLinker* class_linker = runtime->GetClassLinker();
std::string descriptor(NormalizeJniClassDescriptor(name));
ScopedObjectAccess soa(env);
mirror::Class* c = nullptr;
if (runtime->IsStarted()) {
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));
c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
} else {
c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
}
return soa.AddLocalReference<jclass>(c);
}
综上可以看出并不是局部引用不用调用DeleteLocalRef
来释放。而是建议调用一下。如果你的jni方法很简单&与java交互很少也可以不调用。但是如下的一些情况需要手动显示的调用,为了防止内存溢出和局部引用表溢出。
- 如上我们模拟的情况,在for循环里或者其它操作类似频繁创建局部引用的需要释放
- 遍历数组产生的局部引用,用完后要删除。
全局引用
通过调用jobject NewGlobalRef(jobject obj)
基于引用来创建,参数是jobject
类型。它可以跨方法、跨线程使用。JVM不会自动释放它,必须显示调用DeleteGlobalRef手动释放void DeleteGlobalRef(jobject globalRef)
如下使用示例:
在jni中调用java String类构造返回String
// jni_ref.cpp
static jclass g_j_cls; // 加static前缀 只对本源文件可见,对其它源文件隐藏
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jniGlobalRef(JNIEnv *env, jobject instance) {
if (g_j_cls == nullptr) {
jclass local_j_cls = env->FindClass("java/lang/String");
// 将local_j_cls局部引用改为全局引用
g_j_cls = (jclass) env->NewGlobalRef(local_j_cls);
} else {
LOG_D("g_j_cls else");
}
// 调用public String(String value); 构造
jmethodID j_mid = env->GetMethodID(g_j_cls, "<init>", "(Ljava/lang/String;)V");
jstring str = env->NewStringUTF("GlobalRef");
jstring j_str = (jstring) env->NewObject(g_j_cls, j_mid, str);
return j_str;
}
extern "C" JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_delGlobalRef(JNIEnv *env, jobject instance) {
if (g_j_cls != nullptr) {
LOG_D("DeleteGlobalRef");
// 释放某个全局引用
env->DeleteGlobalRef(g_j_cls);
}
}
java调用
public native String jniGlobalRef();
public native void delGlobalRef();
String ret1 = jniRef.jniGlobalRef();
Log.e(TAG, "jniGlobalRef=" + ret1);
String ret2 = jniRef.jniGlobalRef();
Log.e(TAG, "jniGlobalRef=" + ret2);
jniRef.delGlobalRef();
g_j_cls
就是一个全局引用,然后我们多次调用下jniRef.jniGlobalRef
方法打印如下:
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp E/JNI: jniGlobalRef=GlobalRef
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp D/JNI: g_j_cls else
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp E/JNI: jniGlobalRef=GlobalRef
2020-10-16 20:30:46.074 29358-29358/com.bj.gxz.jniapp D/JNI: DeleteGlobalRef
说明全局引用可以起到缓存的效果,为什么要做这个测验呢? 因为频繁调用类似JNI接口FindClass查找java中Class引用时是比较耗性能的,特别是在有交互频繁的JNI的app中。
弱全局引用
这个有点类似于java的软引用SoftReference
,jvm在内存不足的时候会释放它。通过调用jweak NewWeakGlobalRef(jobject obj)
来创建一个弱全局引用,释放调用void DeleteWeakGlobalRef(jweak obj)
,jweak
为typedef _jobject* jweak;
_jobject
指针的别名。
如下使用示例,和全局引用一样把全局引用的方法改为弱全局引用的方法即可。
// jni_ref.cpp
static jclass g_w_j_cls;
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_jniWeakGlobalRef(JNIEnv *env, jobject instance) {
if (g_w_j_cls == nullptr) {
jclass local_j_cls = env->FindClass("java/lang/String");
// 将local_j_clss局部引用改为弱全局引用
g_w_j_cls = (jclass) env->NewWeakGlobalRef(local_j_cls);
} else {
LOG_D("g_w_j_cls else");
}
jmethodID j_mid = env->GetMethodID(g_w_j_cls, "<init>", "(Ljava/lang/String;)V");
// 使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC的类对象
// 检查弱引用是否活动,即引用的比较IsSameObject
// 如果g_w_j_cls指向的引用已经被回收,会返回JNI_TRUE
// 如果仍然指向一个活动对象,会返回JNI_FALSE
jboolean isGC = env->IsSameObject(g_w_j_cls, nullptr);
if (isGC) {
LOG_D("weak reference has been gc");
return env->NewStringUTF("weak reference has been gc");
} else {
jstring str = env->NewStringUTF("WeakGlobalRef");
jstring j_str = (jstring) env->NewObject(g_w_j_cls, j_mid, str);
return j_str;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_delWeakGlobalRef(JNIEnv *env, jobject instance) {
if (g_w_j_cls != nullptr) {
// 调用DeleteWeakGlobalRef来释放它,如果不手动调用这个函数来释放所指向的对象,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。
LOG_D("DeleteWeakGlobalRef");
env->DeleteWeakGlobalRef(g_w_j_cls);
}
}
java调用
String ret3 = jniRef.jniWeakGlobalRef();
Log.e(TAG, "jniWeakGlobalRef=" + ret3);
String ret4 = jniRef.jniWeakGlobalRef();
Log.e(TAG, "jniWeakGlobalRef=" + ret4);
jniRef.delWeakGlobalRef();
g_w_j_cls
就是一个弱全局引用,然后我们多次调用下jniRef.jniWeakGlobalRef
方法打印如下:
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp E/JNI: jniWeakGlobalRef=WeakGlobalRef
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp D/JNI: g_w_j_cls else
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp E/JNI: jniWeakGlobalRef=WeakGlobalRef
2020-10-16 20:30:46.075 29358-29358/com.bj.gxz.jniapp D/JNI: DeleteWeakGlobalRef
和全局引用一样可以起到缓存的效果。
刚才我们说了就是弱全局引用在内存不足的时候会被jvm回收,怎么判断它被回收了,判null ,没错!当被回收了会为null。所以我们在使用弱全局引用的时候频道弱全局引用是否还存在。怎么判断呢?使用引用比较 。
引用比较
在jni中提供了 jboolean IsSameObject(jobject ref1, jobject ref2)
方法。如果ref1和ref2指向同个对象则返回JNI_TRUE
,否则返回JNI_FALSE
。
jclass local_j_cls_1 = env->FindClass("java/util/ArrayList");
jclass local_j_cls_2 = env->FindClass("java/util/ArrayList");
jboolean same1 = env->IsSameObject(local_j_cls_1, local_j_cls_2);
LOG_D("%d",same1);
jboolean same2= env->IsSameObject(local_j_cls_1, nullptr);
LOG_D("%d",same2);
输出 1和0
缓存策略
当我们在本地代码方法中通过FindClass查找Class、GetMethodID查找方法、GetFieldID获取类的字段ID和GetFieldValue获取字段的时候是需要jvm来做很多工作的,可能这个字段ID或者方法是在超类中继承而来的,那jvm可能还需要层次遍历。而这些负责和jni交互java中的类的全路径,字段,方法一般是不会修改了,是固定的。这也是为什么我们在做android混淆打包的时候需要keep这些类,因为这些一般不会变,不能变,变了后jni中会找不到了具体的类,字段,方法了。既然打包后不会变我们是可以进行缓存策略来处理。
另外至于效率提高多少,没有验证,不过不重要,如果是频繁这种查找一般会采用缓存,只查找一次或者在程序初始化的时候提前查找。
对于这类情况的缓存分为基本数据类型缓存和引用缓存。
基本数据类型缓存
基本数据类型的缓存在c,c++中可以借助关键字static
处理。
学过c,c++的都知道
- static局部变量只初始化一次,下一次依据上一次结果值
- static全局变量只初使化一次,防止在其他文件中被引用
- 加static函数的函数为内部函数,只能在本源文件中使用, 和普通函数的作用域不同
static jclass g_j_cls_cache;
extern "C" JNIEXPORT jstring JNICALL
Java_com_bj_gxz_jniapp_ref_JNIRef_refCache(JNIEnv *env, jobject instance) {
if (g_j_cls_cache == nullptr) {
jclass local_j_cls = env->FindClass("java/lang/String");
// 将local_j_cls局部引用改为全局引用
g_j_cls_cache = (jclass) env->NewGlobalRef(local_j_cls);
} else {
LOG_D("g_j_cls_cache use cache");
}
// 调用public String(String value); 构造
static jmethodID j_mid;
if (j_mid == nullptr) {
j_mid = env->GetMethodID(g_j_cls_cache, "<init>", "(Ljava/lang/String;)V");
} else {
LOG_D("j_mid use cache");
}
jstring str = env->NewStringUTF("refCache");
jstring j_str = (jstring) env->NewObject(g_j_cls_cache, j_mid, str);
return j_str;
}
java调用
String ret5 = jniRef.refCache();
Log.e(TAG, "refCache=" + ret5);
String ret6 = jniRef.refCache();
Log.e(TAG, "refCache=" + ret6);
jniRef.delRefCache();
local_j_cls局部引用变为全局引用,j_mid变量改为static
输出:
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp E/JNI: refCache=refCache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp D/JNI: g_j_cls_cache use cache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp D/JNI: j_mid use cache
10-16 22:58:21.074 4469-4469/com.bj.gxz.jniapp E/JNI: refCache=refCache
有人问local_j_cls局部引用可以加static吗?不用全局引用/全局弱应用? 可以加static,但是不能起到缓存的作用。因为上文说了局部引用在函数结束后会被jvm回收了,不然再次使用回到非法内存访问导致应用crash,所以正确的做法如上用全局引用/全局弱应用。
引用类型的缓存
可以借助上面的全局引用或者弱全局引用,弱全局引用记得在使用前判断下是否被回收了IsSameObject
,最后记得释放 DeleteGlobalRef
,DeleteWeakGlobalRef
。
最后源代码:https://github.com/ta893115871/JNIAPP