背景需求
假设现在有这么一个业务需求,我们需要通过JNI在本地方法中干一件耗时操作,干完以后再通知Java层。这个实现逻辑非常简单,就是我们可以在本地方法中开启一个线程做函数操作,然后通过JNI回调Java方法。好了,架构已经定下来了,那么我们一步步实现。在实现过程中我也会将错误思路和实现代码提供出来,让大家对正确的写法更加刻骨铭心。
代码实现探索
我想绝大部分读者刚开始的时候,实现该逻辑的办法是初始化的时候保存JNIEnv和jobject为全局变量,然后在需要的时候直接使用。那我们就先按照该思路进行。
1. 子线程中使用全局的JNIEnv和jobject
重要的事情说三篇,这个方法行不通,行不通,行不通!
Java端代码:
Java本地方法类NativeThread.java代码:
package com.xxx.api.thread;
import android.util.Log;
public class NativeThread {
private static final String TAG = "NativeThread";
public native void nativeInit();//Native方法
//供JNI端回调的Java方法
public void onNativeCallBack(int count) {
Log.e(TAG, "onNativeCallBack : " + count);
}
static {
System.loadLibrary("native_thread");
}
}
Java测试代码:
private void operateNativeThread(){
NativeThread mNativeThread = new NativeThread();
mNativeThread.nativeInit();
}
JNI端代码:
Java中Native方法对应com_xxx_api_thread_NativeThread.h代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xxx_api_thread_NativeThread */
#ifndef _Included_com_xxx_api_thread_NativeThread
#define _Included_com_xxx_api_thread_NativeThread
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_xxx_api_thread_NativeThread
* Method: nativeInit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_xxx_api_thread_NativeThread_nativeInit
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
对应的com_xxx_api_thread_NativeThread.cpp代码如下:
#include "com_xxx_api_thread_NativeThread.h"
#include <stdio.h>
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#define TAG "NativeThread"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
JavaVM *gJavaVM;//全局JavaVM 变量
jobject gJavaObj;//全局Jobject变量
JNIEnv * gEnv; //全局的JNIEnv变量
jmethodID nativeCallback;//全局的方法ID
static int count = 0;
static void* native_thread_exec(void *arg)
{
//线程循环
for(int i = 0 ; i < 5; i++)
{
usleep(20);
//跨线程回调Java层函数
gEnv->CallVoidMethod(gJavaObj,nativeCallback,count++);
}
}
/*
* Class: com_xxx_api_thread_NativeThread
* Method: nativeInit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_xxx_api_thread_NativeThread_nativeInit
(JNIEnv * env, jobject object)
{
LOGE(TAG,"Java_com_xxx_api_thread_NativeThread_nativeInit\n");
gJavaObj = object;//保存object到全局gJavaObj中
gEnv = env;
jclass clazz = env->GetObjectClass(object);
nativeCallback = env->GetMethodID(clazz,"onNativeCallBack","(I)V");
pthread_t id;
//通过pthread库创建线程
LOGE(TAG,"create native thread\n");
if(pthread_create(&id,NULL,native_thread_exec,NULL)!=0)
{
LOGE(TAG,"native thread create fail");
return;
}
for(int i = 0 ; i < 5; i++)
{
usleep(20);
//跨线程回调Java层函数
gEnv->CallVoidMethod(gJavaObj,nativeCallback,count++);
}
LOGE(TAG,"native thread creat success");
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
//获取JNI_VERSION版本
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE(TAG,"checkversion error\n");
return -1;
}
//返回jni 的版本
return JNI_VERSION_1_6;
}
运行演示:
5I/NativeThread(13225): JNI_OnLoad
I/NativeThread(13225): Java_com_xxx_api_thread_NativeThread_nativeInit
I/NativeThread(13225): create native thread
E/NativeThread(13225): onNativeCallBack : 0
E/NativeThread(13225): onNativeCallBack : 1
E/NativeThread(13225): onNativeCallBack : 3
E/NativeThread(13225): onNativeCallBack : 4
E/NativeThread(13225): onNativeCallBack : 5
I/NativeThread(13225): native thread creat success
I/DEBUG ( 302): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG ( 302): Build fingerprint: 'XXX/CB03/CB03:5.1.1/LMY47V/CB03_CH_V4.70_S:user/release-keys'
I/DEBUG ( 302): Revision: '0'
I/DEBUG ( 302): ABI: 'arm'
I/DEBUG ( 302): pid: 13225, tid: 13246, name: com.xxx.jni >>> com.xxx.jni <<<
I/DEBUG ( 302): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x98
I/DEBUG ( 302): r0 00000000 r1 a5627dd0 r2 fffffa94 r3 00000007
I/DEBUG ( 302): r4 b6e9ede4 r5 b4ba0000 r6 00000001 r7 fffffa98
I/DEBUG ( 302): r8 b4b8ba28 r9 b4b9dc84 sl 00000000 fp a5627d44
I/DEBUG ( 302): ip b4b9dc9c sp a5627a30 lr b4960057 pc b495fb18 cpsr 800f0030
I/DEBUG ( 302):
I/DEBUG ( 302): backtrace:
I/DEBUG ( 302): #00 pc 000afb18 /system/lib/libart.so (_ZN3artL8JniAbortEPKcS1_+47)
I/DEBUG ( 302): #01 pc 000b046f /system/lib/libart.so (_ZN3art9JniAbortFEPKcS1_z+58)
I/DEBUG ( 302): #02 pc 000b316f /system/lib/libart.so (_ZN3art11ScopedCheckC2EP7_JNIEnviPKc+334)
I/DEBUG ( 302): #03 pc 000ba841 /system/lib/libart.so (_ZN3art8CheckJNI15CallVoidMethodVEP7_JNIEnvP8_jobjectP10_jmethodIDSt9__va_list+32)
I/DEBUG ( 302): #04 pc 00000d79 /data/app/com.xxx.jni-2/lib/arm/libnative_thread.so (_ZN7_JNIEnv14CallVoidMethodEP8_jobjectP10_jmethodIDz+16)
I/DEBUG ( 302): #05 pc 00000db3 /data/app/com.xxx.jni-2/lib/arm/libnative_thread.so
I/DEBUG ( 302): #06 pc 000132b3 /system/lib/libc.so (_ZL15__pthread_startPv+30)
I/DEBUG ( 302): #07 pc 000111df /system/lib/libc.so (__start_thread+6)
I/DEBUG ( 302):
I/DEBUG ( 302): Tombstone written to: /data/tombstones/tombstone_06
很不幸的事情发生了,程序直接闪退并打印出异常堆栈信息。这是为什么呢,为什么子线程中使用全局的 JNIEnv 和 jobject 不行呢?下面让我们来分析一番。
异常分析:
通过前面的代码我们可以看到在C++的本地方法和开启线程中回调Java方法是有所不同的。而造成这种局面的原因是由于JNIEnv *env的使用限制和局部引用造成的。JNIEnv *env是接口指针,通过它能调用JNI所有函数来使用虚拟机的各种功能,但是它是一个指向线程的局部引用数据,不能被保存起来供其它线程使用,它与线程是一一对应的关系,每个线程都有一个属于自己的JNIEnv * env。既然JNIEnv * env不能被多线程共享,而JavaVM是可以的(可以参见篇章JNI/NDK入门指南之JavaVM和JNIEnv的介绍),并且JavaVM的的结构中可以通过AttachCurrentThread获取当前线程中的JNIEnv指针,那么我让我们试试通过保存JavaVM,然后提供给其它线程使用,然后在其它线程中通过JavaVM拿到env。
2. 在主线程中保存JavaVM和jobject,然后在子线程中调用
重要的事情说三篇,这个方法行不通,行不通,行不通!
在前面的子线程中使用全局的JNIEnv和jobject已经验证失败了,这里我们将继续尝试在主线程中保存JavaVM和jobject,然后在子线程中调用。
#include "com_xxx_api_thread_NativeThread.h"
#include <stdio.h>
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#define TAG "NativeThread"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
JavaVM *gJavaVM = NULL;//全局JavaVM 变量
jobject gJavaObj = NULL;//全局Jobject变量
JNIEnv * gEnv = NULL; //全局的JNIEnv变量
jmethodID nativeCallback = NULL;//全局的方法ID
static int count = 0;
static void* native_thread_exec(void *arg)
{
JNIEnv * env;
if(gJavaVM != NULL)
{
if(gJavaVM->AttachCurrentThread(&env, NULL) == JNI_OK)
{
if(env != NULL)
{
//线程循环
for(int i = 0 ; i < 5; i++)
{
usleep(20);
//跨线程回调Java层函数
env->CallVoidMethod(gJavaObj,nativeCallback,count++);
}
}
}
}
}
/*
* Class: com_xxx_api_thread_NativeThread
* Method: nativeInit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_xxx_api_thread_NativeThread_nativeInit
(JNIEnv * env, jobject object)
{
LOGE(TAG,"Java_com_xxx_api_thread_NativeThread_nativeInit\n");
gJavaObj = object;//保存object到全局gJavaObj中
gEnv = env;
jclass clazz = env->GetObjectClass(object);
nativeCallback = env->GetMethodID(clazz,"onNativeCallBack","(I)V");
//操作方式二,调用JNI函数保存JavaVM
env->GetJavaVM(&gJavaVM);
pthread_t id;
//通过pthread库创建线程
LOGE(TAG,"create native thread\n");
if(pthread_create(&id,NULL,native_thread_exec,NULL)!=0)
{
LOGE(TAG,"native thread create fail");
return;
}
for(int i = 0 ; i < 5; i++)
{
usleep(20);
//跨线程回调Java层函数
gEnv->CallVoidMethod(gJavaObj,nativeCallback,count++);
}
LOGE(TAG,"native thread creat success");
}
//SO动态库加载一定会在该流程
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGE(TAG,"JNI_OnLoad\n");
JNIEnv* env = NULL;
//获取JNI_VERSION版本
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE(TAG,"checkversion error\n");
return -1;
}
//操作方式一,通过SO加载时保存全局JavaVM
//gJavaVM = vm;
//返回jni 的版本
return JNI_VERSION_1_6;
}
在这里我们定义了一个全局的JavaVM和jobject
JavaVM *gJavaVM = NULL;//全局JavaVM 变量
jobject gJavaObj = NULL;//全局Jobject变量
然后我们可以使用如下两种方式保存全局的JavaVM:
- 第一种实在加载SO库的时候,在默认函数JNI_OnLoad里面保存,代码如下:
//SO动态库加载一定会在该流程
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGE(TAG,"JNI_OnLoad\n");
JNIEnv* env = NULL;
//获取JNI_VERSION版本
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE(TAG,"checkversion error\n");
return -1;
}
//操作方式一,通过SO加载时保存全局JavaVM
//gJavaVM = vm;
//返回jni 的版本
return JNI_VERSION_1_6;
}
- 第二种就是在调用本地方法初始化中,如下:
/*
* Class: com_xxx_api_thread_NativeThread
* Method: nativeInit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_xxx_api_thread_NativeThread_nativeInit
(JNIEnv * env, jobject object)
{
...
//操作方式二,调用JNI函数保存JavaVM
env->GetJavaVM(&gJavaVM);
...
}
线程中使用:
static void* native_thread_exec(void *arg)
{
JNIEnv * env;
if(gJavaVM != NULL)
{
if(gJavaVM->AttachCurrentThread(&env, NULL) == JNI_OK)
{
if(env != NULL)
{
//线程循环
for(int i = 0 ; i < 5; i++)
{
usleep(20);
//跨线程回调Java层函数
env->CallVoidMethod(gJavaObj,nativeCallback,count++);
}
}
}
}
}
运行演示:
I/NativeThread(13732): JNI_OnLoad
I/NativeThread(13732): Java_com_xxx_api_thread_NativeThread_nativeInit
I/NativeThread(13732): create native thread
E/NativeThread(13732): onNativeCallBack : 0
E/NativeThread(13732): onNativeCallBack : 1
E/NativeThread(13732): onNativeCallBack : 2
E/NativeThread(13732): onNativeCallBack : 3
E/NativeThread(13732): onNativeCallBack : 4
I/NativeThread(13732): native thread creat success
I/DEBUG ( 302): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG ( 302): Build fingerprint: 'XXX/CB03/CB03:5.1.1/LMY47V/CB03_CH_V4.70_S:user/release-keys'
I/DEBUG ( 302): Revision: '0'
I/DEBUG ( 302): ABI: 'arm'
I/DEBUG ( 302): pid: 13732, tid: 13751, name: Thread-457 >>> com.xxx.jni <<<
I/DEBUG ( 302): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x14
I/DEBUG ( 302): Abort message: 'art/runtime/check_jni.cc:65] JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0xbed2deec'
I/DEBUG ( 302): r0 29627015 r1 00000000 r2 00000000 r3 fffffa9c
I/DEBUG ( 302): r4 b4b9dc84 r5 70e70420 r6 b6e9ede4 r7 00000000
I/DEBUG ( 302): r8 70e7043c r9 00000000 sl 00000000 fp b6e9ede4
I/DEBUG ( 302): ip b4b9b608 sp a5627798 lr b4ad306d pc b4a8746c cpsr a0070030
I/DEBUG ( 302):
I/DEBUG ( 302): backtrace:
I/DEBUG ( 302): #00 pc 001d746c /system/lib/libart.so (_ZN3art6mirror9ArtMethod7ToDexPcEjb+31)
I/DEBUG ( 302): #01 pc 00223069 /system/lib/libart.so (_ZN3art16StackDumpVisitor10VisitFrameEv+72)
I/DEBUG ( 302): #02 pc 0021e3e9 /system/lib/libart.so (_ZN3art12StackVisitor9WalkStackEb+260)
I/DEBUG ( 302): #03 pc 00223fab /system/lib/libart.so (_ZNK3art6Thread13DumpJavaStackERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+170)
I/DEBUG ( 302): #04 pc 002255d9 /system/lib/libart.so (_ZNK3art6Thread4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+156)
I/DEBUG ( 302): #05 pc 0022e6c1 /system/lib/libart.so (_ZN3art10ThreadList10DumpLockedERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+104)
I/DEBUG ( 302): #06 pc 002159cf /system/lib/libart.so (_ZN3art10AbortState4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+238)
I/DEBUG ( 302): #07 pc 00215c11 /system/lib/libart.so (_ZN3art7Runtime5AbortEv+72)
I/DEBUG ( 302): #08 pc 000a62c5 /system/lib/libart.so (_ZN3art10LogMessageD1Ev+1312)
I/DEBUG ( 302): #09 pc 000aff25 /system/lib/libart.so (_ZN3artL8JniAbortEPKcS1_+1084)
I/DEBUG ( 302): #10 pc 000b046f /system/lib/libart.so (_ZN3art9JniAbortFEPKcS1_z+58)
I/DEBUG ( 302): #11 pc 000b27a1 /system/lib/libart.so (_ZN3art11ScopedCheck5CheckEbPKcz.constprop.129+668)
I/DEBUG ( 302): #12 pc 000ba853 /system/lib/libart.so (_ZN3art8CheckJNI15CallVoidMethodVEP7_JNIEnvP8_jobjectP10_jmethodIDSt9__va_list+50)
I/DEBUG ( 302): #13 pc 00000d81 /data/app/com.xxx.jni-1/lib/arm/libnative_thread.so (_ZN7_JNIEnv14CallVoidMethodEP8_jobjectP10_jmethodIDz+16)
I/DEBUG ( 302): #14 pc 00000dd5 /data/app/com.xxx.jni-1/lib/arm/libnative_thread.so
I/DEBUG ( 302): #15 pc 000132b3 /system/lib/libc.so (_ZL15__pthread_startPv+30)
I/DEBUG ( 302): #16 pc 000111df /system/lib/libc.so (__start_thread+6)
I/DEBUG ( 302):
I/DEBUG ( 302): Tombstone written to: /data/tombstones/tombstone_09
结果分析:
可以看到依然直接crash失败,提示jobject reference无效。按道理说应该要OK了,但是为什么依然失败了呢?在这里我也不卖关子了,这是因为要想在新线程中使用jclass或者jobject就必须以全局引用方式保存,也许读者会说我们不是以前将jobject全局保存了吗,我们是全局保存了,但是我们保存的jobject是一个局部引用,一旦我们的函数Java_com_xxx_api_thread_NativeThread_nativeInit返回,jobject就会被GC回收销毁,所以此时虽然我们的gJavaObj保存了全局引用,但是它现在指向的是一个非法地址,当然我们使用非法地址直接报crash。那么有什么解决办法呢,那就是根据局部引用创建全局引用,这样就不会被GC回收销毁了。
3. 在主线程中保存JavaVM和并且创建全局jobject引用,然后在子线程中调用
人狠话不多,直接上代码:
#include "com_xxx_api_thread_NativeThread.h"
#include <stdio.h>
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#define TAG "NativeThread"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
JavaVM *gJavaVM = NULL;//全局JavaVM 变量
jobject gJavaObj = NULL;//全局Jobject变量
jmethodID nativeCallback = NULL;//全局的方法ID
static int count = 0;
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();
LOGE(TAG,"thread stoped");
return ((void *)0);
}
/*
* Class: com_xxx_api_thread_NativeThread
* Method: nativeInit
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_xxx_api_thread_NativeThread_nativeInit
(JNIEnv * env, jobject object)
{
LOGE(TAG,"Java_com_xxx_api_thread_NativeThread_nativeInit\n");
gJavaObj = env->NewGlobalRef(object);//创建全局引用
jclass clazz = env->GetObjectClass(object);
nativeCallback = env->GetMethodID(clazz,"onNativeCallBack","(I)V");
//操作方式二,调用JNI函数保存JavaVM
env->GetJavaVM(&gJavaVM);
pthread_t id;
//通过pthread库创建线程
LOGE(TAG,"create native thread\n");
if(pthread_create(&id,NULL,native_thread_exec,NULL)!=0)
{
LOGE(TAG,"native thread create fail");
return;
}
for(int i = 0 ; i < 5; i++)
{
usleep(20);
//跨线程回调Java层函数
env->CallVoidMethod(gJavaObj,nativeCallback,count++);
}
LOGE(TAG,"native thread creat success");
}
//SO动态库加载一定会在该流程
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGE(TAG,"JNI_OnLoad\n");
JNIEnv* env = NULL;
//获取JNI_VERSION版本
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE(TAG,"checkversion error\n");
return -1;
}
//操作方式一,通过SO加载时保存全局JavaVM
//gJavaVM = vm;
//返回jni 的版本
return JNI_VERSION_1_6;
}
运行演示:
I/NativeThread(14716): JNI_OnLoad
I/NativeThread(14716): Java_com_xxx_api_thread_NativeThread_nativeInit
I/NativeThread(14716): create native thread
E/NativeThread(14716): onNativeCallBack : 0
E/NativeThread(14716): onNativeCallBack : 1
E/NativeThread(14716): onNativeCallBack : 2
I/NativeThread(14716): nativeThreadExec
I/NativeThread(14716): The pthread id : -1205608352
E/NativeThread(14716): onNativeCallBack : 3
E/NativeThread(14716): onNativeCallBack : 4
E/NativeThread(14716): onNativeCallBack : 5
I/NativeThread(14716): native thread creat success
E/NativeThread(14716): onNativeCallBack : 6
E/NativeThread(14716): onNativeCallBack : 7
E/NativeThread(14716): onNativeCallBack : 8
E/NativeThread(14716): onNativeCallBack : 9
I/NativeThread(14716): thread stoped
总结思考
通过前面的一个坑一个脚印的摸索,读者应该对JNI多线程回调Java方法应该是了然于心了。老规矩还是总结一下,对于在JNI多线程中使用JNI应该注意哪些地方:
- 线程之间不能直接传递JNIEnv和jobject这类通过JNI函数传递下来的属性值,因为他们和线程有关系,且属于局部引用在函数调用结束后会被GC回收并且销毁。
- JavaVM是可以进行传递的,因为它属于JNI进程的,每个进程有且只有一个JavaVM所以可以被多线程共享,但是JNIEnv和jobject是属于线程私有的,不能共享。
- 所以在多线程中需要使用jobject和JNIEnv的解决办法就是保存JavaVM,并且创建全局jobject引用,然后使用AttachCurrentThread从JavaVM中获取JNIEnv。