1. 背景
在JNI中使用中需要在一个c层的回调方法中调用Java层的静态方法,一开始的设想是初始化的时候保存JNIEvn与jclass为全局变量,需要的时候直接使使用。在实际使用中发现,直接使用会出现奔溃。
初步猜测可能是多线程引起。
2. 问题排查
2.1 步骤1 子线程中使用全局JNIEnv与jclass
使用方法
jmethodID mid = (*g_env)->GetStaticMethodID(g_env, g_cls, "print", "(Ljava/lang/String;)V");
jstring param = (*g_env)->NewStringUTF(g_env, str);
(*g_env)->CallStaticVoidMethod(g_env, g_cls, mid, param);
(*g_env)->DeleteLocalRef(g_env, param);
异常现象
程序直接闪退,无任何异常日志。
原因
在C中直接调用与开启线程调用java方法是有所不同,这是由JNIEnv *env的使用限制引起的。 JNIEnv *env是接口指针,通过它能调用JNI所有函数来使用虚拟机的各种功能,它是一个指向线程的局部数据,不能被保存来供其它线程使用,它与线程是一一对应对应关系,每个线程都可以获取一个属于自己的JNIEnv *env。
env
不能多线程共享,而JavaVM
可以,所以要通过在JNI入口c文件下把JavaVM保存起来,提供给其他线程使用,然后就可以在其他线程中通过JavaVM来拿到env。
2.2 步骤2 保存JavaVM,子线程中调用
定义全局JavaVM
static JavaVM *g_jvm = NULL;
在保存全局JVM
// 保存全局JVM以便在子线程中使用
(*env)->GetJavaVM(env, &g_jvm);
使用
if (g_jvm != NULL) {
JNIEnv *env;
jmethodID mid;
jclass cls;
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) {
if (env != NULL) {
cls = (*env)->FindClass(env, "com/sharezer/utils/JniUtils");
mid = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");
jstring param = (*env)->NewStringUTF(env, str);
(*env)->CallStaticVoidMethod(env, cls, mid, param);
(*env)->DeleteLocalRef(env, param);
}
}
}
运行结果
子线程调用时程序奔溃,奔溃日志如下,
A/art: art/runtime/check_jni.cc:65] JNI DETECTED ERROR IN APPLICATION: JNI GetStaticMethodID called with pending exception 'java.lang.ClassNotFoundException' thrown in unknown throw location
art/runtime/check_jni.cc:65] in call to GetStaticMethodID
排查后发现相FindClass,到不到自定义的类。
推测
子线程AttachCurrentThread得到的env其类的加载器中并没有去加载自定义的类,所有这里你无法去FindClass你自己的类。
2.3 保存全局JavaVM与jclass
//保存全局JVM以便在子线程中使用
(*env)->GetJavaVM(env, &g_jvm);
g_cls = (*env)->FindClass(env, "com/sharezer/utils/JniUtils");
使用
if (g_jvm != NULL) {
JNIEnv *env;
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) {
if (env != NULL && g_cls != NULL) {
jmethodID mid = (*env)->GetStaticMethodID(env, g_cls, "print", "(Ljava/lang/String;)V");
jstring param = (*env)->NewStringUTF(env, str);
(*env)->CallStaticVoidMethod(env, g_cls, mid, param);
(*env)->DeleteLocalRef(env, param);
}
}
}
运行结果
A/art: art/runtime/check_jni.cc:65] JNI DETECTED ERROR IN APPLICATION: jclass is an invalid local reference: 0x100001 (0xdead4321)
分析
jclass本地引用无效, 这个是与android5.0的GC机制有关系。要想在新线程中使用jclass或jobject,就必须以全局引用方式保存,否则jclass只是局部引用,一旦函数返回,jclass就会被GC回收销毁,jclass指向的就是一个非法地址,最终导致上面的JNI错误。
解决方法
(*env)->GetJavaVM(env, &g_jvm);
g_cls = (*env)->FindClass(env, "com/sharezer/utils/JniUtils");
// 创建全局引用
g_cls = (*env)->NewGlobalRef(env, g_cls);
运行正常。
3. 总结
线程间不能直接传递JNIEnv和jobject这类线程专属属性值,JavaVM是属于java进程的,每个进程只有一个JavaVM,而这个JavaVM可以被多线程共享,但是JNIEnv和jobject是属于线程私有的,不能共享。解决方法就是保存JavaVM与全局引用的jclass,再使用AttachCurrentThread
从JavaVM取到当前线程JNIEnv。如果需要传递jobject,方法与jclass一样。