登录
  • 欢迎访问 Sharezer Blog

JNI中多线程回调Java方法问题

Android sharezer 4190次浏览 已收录 0个评论

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一样。


Sharezer , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明JNI中多线程回调Java方法问题
喜欢 (5)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址