JNI 编程上手指南之多线程

7/3/2023

# 核心要点

JNI 环境下,进行多线程编程,有以下两点是需明确的:

  • JNIEnv 是一个线程作用域的变量,不能跨线程传递,每个线程都有自己的 JNIEnv 且彼此独立
  • 局部引用不能在本地函数中跨函数使用,不能跨线程使用,当然也不能直接缓存起来使用

# 示例程序

示例程序主要演示:

  • 如何在子线程获取到属于子线程自己的 JNIEnv
  • 上面说了局部引用不能再线程之间直接传递,所以我们只有另觅他法。

Java 层:

public void javaCallback(int count) {
    Log.e(TAG, "onNativeCallBack : " + count);
}

public native void threadTest();
1
2
3
4
5

Native 层:

static int count = 0;
JavaVM *gJavaVM = NULL;//全局 JavaVM 变量
jobject gJavaObj = NULL;//全局 Jobject 变量
jmethodID nativeCallback = NULL;//全局的方法ID

//这里通过标志位来确定 两个线程的工作都完成了再执行 DeleteGlobalRef
//当然也可以通过加锁实现
bool main_finished = false;
bool background_finished = false;

static void *native_thread_exec(void *arg) {

    LOGE(TAG, "nativeThreadExec");
    LOGE(TAG, "The pthread id : %d\n", pthread_self());
    JNIEnv *env;
    //从全局的JavaVM中获取到环境变量
    gJavaVM->AttachCurrentThread(&env, NULL);

    //线程循环
    for (int i = 0; i < 5; i++) {
        usleep(2);
        //跨线程回调Java层函数
        env->CallVoidMethod(gJavaObj, nativeCallback, count++);
    }
    gJavaVM->DetachCurrentThread();

    background_finished = true;

    if (main_finished && background_finished) {
        env->DeleteGlobalRef(gJavaObj);
        LOGE(TAG, "全局引用在子线程销毁");
    }

    return ((void *) 0);

}


extern "C"
JNIEXPORT void JNICALL
Java_com_yuandaima_myjnidemo_MainActivity_threadTest(JNIEnv *env, jobject thiz) {
    //创建全局引用,方便其他函数或线程使用
    gJavaObj = env->NewGlobalRef(thiz);
    jclass clazz = env->GetObjectClass(thiz);
    nativeCallback = env->GetMethodID(clazz, "javaCallback", "(I)V");
    //保存全局 JavaVM,注意 JavaVM 不是 JNI 引用类型
    env->GetJavaVM(&gJavaVM);

    pthread_t id;
    if (pthread_create(&id, NULL, native_thread_exec, NULL) != 0) {
        return;
    }

    for (int i = 0; i < 5; i++) {
        usleep(20);
        //跨线程回调Java层函数
        env->CallVoidMethod(gJavaObj, nativeCallback, count++);
    }

    main_finished = true;

    if (main_finished && background_finished && !env->IsSameObject(gJavaObj, NULL)) {
        env->DeleteGlobalRef(gJavaObj);
        LOGE(TAG, "全局引用在主线程销毁");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

示例代码中,我们的子线程需要使用主线程中的 jobject thiz,该变量是一个局部引用,不能赋值给一个全局变量然后跨线程跨函数使用,我们通过 NewGlobalRef 将局部引用装换为全局引用并保存在全局变量 jobject gJavaObj 中,在使用完成后我们需要使用 DeleteGlobalRef 来释放全局引用,因为多个线程执行顺序的不确定性,我们使用了标志位来确保两个线程所有的工作完成后再执行释放操作。

JNIEnv 是一个线程作用域的变量,不能跨线程传递,每个线程都有自己的 JNIEnv 且彼此独立,实际开发中,我们通过以下代码:

JavaVM *gJavaVM = NULL;
//主线程获取到 JavaVM
env->GetJavaVM(&gJavaVM);

//子线程通过 JavaVM 获取到自己的 JNIEnv
JNIEnv *env;
gJavaVM->AttachCurrentThread(&env, NULL);
1
2
3
4
5
6
7

在子线程中获取到 JNIEnv。JavaVM 是一个普通指针,由 JVM 来管理其内存的分配与回收,不是 JNI 引用类型,所以 我们可以把它赋值给一个全局变量,直接用,也不用考虑他的内存分配与后手问题。