0 明确一些名词
前面的文章中对于java层 、本地层、JNI层的定义似乎说的有些模糊,这里我按我的理解再定义一下,以便于更好的理解JNI和文章内容。
java层就是用java语言写的逻辑,具体体现就是工程中的java文件。
本地层就是用C/C++语言所写的逻辑,具体体现是工程中的C/C++文件。
JNI层就比较抽象了,我把它定义成 使用到JNI标准(JNI定义的接口或类型)的相关逻辑,包括使用到JNI标准的C/C++函数 和 与java native映射的 C/C++函数,具体体现是使用到JNI标准的C/C++文件,一般情况下,为了保证代码整洁,我们只会在与java native方法映射的C/C++函数所在的文件中集中使用JNI标准,那么此时,JNI层的具体存在就是这个文件,这样一来,JNI层的作用就好理解了,就是通过使用JNI接口,完成java层和C/C++层的相互访问。
1 JNI层的线程
JNI标准没有定义创建线程的接口,故不能通过JNI接口在JNI层创建线程,在JNI层可以通过C/C++创建本地线程,通过反射调用java方法创建java线程,但JNI层无法定义java线程的内容,所以想在JNI层创建线程并定义线程执行的内容,只能通过C/C++创建的本地线程。
前面的内容中,我们没有额外创建本地线程,包含JNI接口的逻辑都是在与java native方法映射的C/C++方法中实现的,所以都是在调用java native方法的线程中使用JNI接口,JNIEnv指针也是自动传进来的,与调用java native方法的java线程唯一相关,这意味着,在JNI层手动创建的本地线程中,不能使用与java native方法映射的本地方法 通过形参传进来的JNIEnv指针,那么在JNI层创建的本地线程中,如何获取与这个线程相关的JNIEnv实例,进而使用JNI接口呢?我们往下看~
2 在JNI层创建本地线程
这里简单介绍一下,如何用C/C++在Android平台创建本地线程。
(1)背景介绍
C++11之前,window和linux平台分别有各自的多线程标准,使用C++编写的多线程往往是依赖于特定平台的。
• Windows平台提供用于多线程创建和管理的win32 api;
• Linux下则有POSIX多线程标准,Threads或Pthreads库提供的API可以在类Unix上运行(包括Android平台);
在C++11新标准中,可以简单通过使用thread库,来管理多线程。thread库可以看做对不同平台多线程API的一层包装;因此使用新标准提供的线程库编写的程序是跨平台的。
综上,在Android平台用C++创建线程有两种方法用 平台相关的Linux的Pthreads库 和 跨平台C++11新标准中的thread库。
(2)使用Linux的pthread库
简要介绍利用pthread库创建一个简单的本地线程使用方法 和 步骤
1)相关函数
下面我们简单介绍下pthread库中比较常用的4个函数,创建一个简单的本地线程,只需要pthread_create 和 pthread_exit 函数即可,另外两个就是多了解了一些,也记录上了,该库中其他函数这里不做介绍了,感兴趣的同学可以自行了解。
/**
* 作用:创建一个线程,并运行
* @param __pthread_ptr 线程id
* @param __attr 线程属性,可以为null
* @param __start_routine 线程执行的函数
* @param __args 线程要执行函数的参数,是一个任意类型指针, 如要传多个参数, 可以用结构封装.
* @return 成功返回0,否则返回错误编号
*/
int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void* __args);
/**
* 作用:结束线程,但并不一定释放资源。
* 当线程为joinable时,作用为:只能结束线程,不能释放线程占用的资源,直到与其相关的第一个join方法调用完毕。
* 当线程为unjoinable时,作用为:结束线程,并释放资源。
* @param __return_value 线程返回值,任意类型指针
*
* 注意:与pthread_join联合使用,可实现线程之间的结果值传递
*/
void pthread_exit(void* __return_value);
/**
* 作用:阻塞调用线程,等待指定线程执行结束后,得到指定线程的返回值并继续执行
* @param __pthread 指定等待线程的id
* @param __return_value_ptr 被等待线程的返回值,一般由被等待线程调用pthread_exit函数将返回值传给pthread_join函数的该形参
* @return 等待成功 返回0,失败返回错误码
*
* 注意:若某线程a被多个其他线程等待(join),只有一个且是第一个等待a线程的线程能返回成功,其他线程的join都会失败,因为
* 只要有一个等待a的pthread_join函数被调用成功,线程a就会释放资源,导致其他join a的线程,join失败。
* 注意:线程不能自己join自己(即在线程执行的方法内join自己)!!
*/
int pthread_join(pthread_t __pthread, void** __return_value_ptr);
/**
* 作用:使指定线程成为unjoinable状态,即取消所有对指定线程的等待(join)
* @param __pthread 指定线程的id
* @return 成功返回0;失败返回错误号
*
* 注意:可用于线程结束后马上释放占用资源;
* 注意:线程可以自己detach自己,也可以通过其他线程detach自己
*/
int pthread_detach(pthread_t __pthread);
2)创建线程基本步骤
//1 引入头文件
#include "pthread.h"
//2 定义一个线程id,
//注意:pthread_t是long类型的别名
//注意:每个线程都需要一个线程id
pthread_t pthread;
//3 定义一个线程要执行的函数
void *normalCallBack(void *data) {
//TODO:自己的逻辑
//5 用于退出线程
pthread_exit(nullptr);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_testCPlusThread(JNIEnv *env, jclass clazz){
//4 创建并运行线程
//注意:创建后就自动开始运行
int result = pthread_create(&pthread, NULL, normalCallBack, NULL);
return result;
}
(3)使用C++的thread库
简要介绍利用C++11 的thread库,创建一个简单的本地线程使用方法 和 步骤
1)相关函数
我们介绍比较常用的4个函数,这些函数是定义在thread库 的 thread类里,具体如下:
class _LIBCPP_TYPE_VIS thread
{
.......
/**
* 创建线程,并启动线程执行指定的函数
* @param __f 需要在线程中执行的函数
* @param __args 函数的参数
*/
thread(_Fp&& __f, _Args&&... __args);
/**
* 作用:判断当前线程是否为joinable的
* @return
*/
bool joinable();
/**
* 作用:将主调线程join到被调用线程,调用后,主调线程会阻塞到这句,
* 直到被调用线程执行完毕;被调线程执行完join后,才释放资源
*/
void join();
/**
* 作用:将join到被调线程的所有线程都取消join,被调线程执行完即释放资源
*/
void detach();
/**
* 作用:获取线程ID
* @return __thread_id类的实例,该类只有一个属性就是 long类型的id
*/
id get_id();
.......
};
2)创建线程基本步骤
//1 引入C++11 thread头文件
#include <thread>
//2 定义线程要执行的任务
void runnable(int args) {
__android_log_write(ANDROID_LOG_ERROR, "yy", std::to_string(args).c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_testCPlusThread(JNIEnv *env, jclass clazz){
int a = 9;
//3 实例化线程,并启动
std::thread t1(runnable,a);
//4 detach线程,即让该线程不能被join
t1.detach();
return 1;
}
注意:
(1) 实例化线程后,必须调用该线程的join函数 或 detach函数,否则程序报错崩溃。
(2)C++的函数如果定义了返回值,一点要返回相应类型的值,否则程序报错崩溃。
3 在JNI层创建的线程中使用JNI接口
(1)步骤
1)获取JavaVM实例
2)将当前线程绑定到JavaVM实例,并获取与该线程一一对应的JNIEnv实例
3)解绑
(2)获取JavaVM实例
在android中,一个进程唯一对应一个JavaVM实例,故进程的多个线程共享这个JavaVM实例;获取JavaVM的方法有三种,我们一般使用其中两种,具体如下:
1)通过JNI_OnLoad方法
当Java层调用System.loadLibrary()方法加载so库时,首先会查找该so库中是否定义了JNI_OnLoad()函数,如果定义了则调用。JNI_OnLoad函数像是加载so库的初始化方法,我们可以自定义其中的逻辑,这个在前面的文章动态注册中进行了详细介绍,另外,我们可以通过该函数的形参获取到JavaVM实例,这是我们今天讨论的重点,具体步骤如下:
//1 定义全局变量,用于持久化JavaVM实例
JavaVM *g_vm = nullptr;
//2 根据声明,定义JNI_OnLoad函数
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
//3 获取JavaVM实例
g_vm = vm;
return JNI_VERSION_1_6;
}
2)通过JNIEnv的GetJavaVM方法获取
GetJavaVM方法说明如下:
/**
* 作用:获取JavaVM实例
* @param vm 指向 JavaVM类型指针变量 的指针
* @return 成功返回0,失败返回负数
*/
jint GetJavaVM(JavaVM** vm);
通过GetJavaVM方法获取JavaVM实例:
//1 定义全局变量,用于持久化JavaVM实例
JavaVM *g_vm = nullptr;
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_testCPlusThread(JNIEnv *env, jclass clazz){
//2 获取
env->GetJavaVM(&g_vm);
return 1;
}
3)通过JNI_CreateJavaVM方法获取
该方法一般由系统调用,用于创建JavaVM,这里不多做介绍了。
(2)获取JNIEnv
得到了JavaVM实例后,我们就可以通过JavaVM结构体定义的方法,获取与线程相关的JNIEnv实例了。
相关方法说明,以下方法都是JavaVM结构体中定义的,需要用JavaVM实例调用:
/**
* 作用:获取当前线程(本地子线程、java子线程、java主线程)对应的JNIEnv
* @param env JNIEnv指针变量的指针,若获取成功 将该形参指向当前线程的JNIEnv实例,若获取失败NULL
* @param version JNI版本
* @return 返回值可能为:
* JNI_EDETACHED(-2): 若当前线程没有绑定到JavaVM,返回JNI_EDETACHED,且形参env为NULL。
* JNI_EVERSION(-3): 若version为不支持的版本号,返回JNI_EVERSION,且形参env为NULL。
* JNI_OK(0): 当前线程已经绑定到JavaVM 且 版本号支持,返回JNI_OK,且形参env指向有效的JNIEnv实例的指针变量
*
* @exceptions 不报异常
*
* 注意:只能获取到已经绑定到JavaVM的线程的JNIEnv实例,如Java
* 线程的JNIEnv实例
*/
jint GetEnv(void** env, jint version);
/**
* 作用:将当前 本地子线程 绑定到JavaVM,并获取JNIEnv实例
* @param p_env JNIEnv类型指针变量指针
* @param thr_args 线程参数,用来指定要绑定的线程,是一个结构体,传NULL表示当前线程
* @return 成功,返回 JNI_OK; 失败,返回合适的JNI错误代码 (负值).
*
* @exceptions 不报异常
*
* 注意:同一个本地子线程只能绑定一个JavaVM实例
* 注意:不要在java线程中使用该方法,即不要在java native方法对应的C/C++函数所在的线程中使用,
* 因为java线程在java层创建的时候就已经绑定
* 注意:与DetachCurrentThread成对使用
*/
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args);
/**
* 作用:将当前 本地子线程 与JavaVM解绑
* @return 成功,返回 JNI_OK; 失败,返回合适的JNI错误代码 (负值).
*
* @exceptions 不报异常
*
* 注意:只能用于解绑本地子线程,因为主线程无法与JavaVM解绑,每个活着的JavaVM必须有一个主线程。
* 注意:本地子线程与JavaVM解绑后,会释放其持有的所有JNI同步锁。
* 注意:不要在java线程中使用该方法,即不要在java native方法对应的C/C++函数所在的线程中使用,
* 这样会提前结束java线程,导致java线程未执行完逻辑就结束。
* 注意:与AttachCurrentThread成对使用
*/
jint DetachCurrentThread();
获取JNIEnv实例步骤:
/**************************************** 自定义方法 ***********************************************/
/**
* 自定义函数
* 作用:获取与当前线程(可以是本地子线程、java子线程、java主线程)对应的JNIEnv实例
* @param jvm JavaVM实例
* @param isNativeThread 出参,用于表示当前线程是否为本地线程
* @return JNIEnv实例 或 NULL
*
* 注意:使用完记得调用detachCurrentThread方法,detachCurrentThread方法只对本地子线程有效,
* 调用后,可释放当前本地子线程持有的JNI同步锁(如果持有了JNI同步锁)
*/
JNIEnv * getCurrentThreadJNIEnv(JavaVM * jvm,jboolean * isNativeThread){
//1 定义一个JNIEnv类型指针变量
JNIEnv *env = nullptr;
if(nullptr == jvm){
return env;
}
int result;
//2 获取当前线程JNIEnv实例
result = jvm->GetEnv((void **) &env, JNI_VERSION_1_6);
if(JNI_EDETACHED == result){//若当前子线程没有绑定到JavaVM(说明当前子线程是本地的)
//3 将当前本地子线程绑定到JavaVM,并获取与当前子线程对应的JNIEnv实例
jvm->AttachCurrentThread(&env, nullptr);
*isNativeThread = true;
}else{//result有可能是JNI_OK 或 JNI_EVERSION
*isNativeThread = false;
}
return env;
}
/**
* 自定义函数
* 作用:将当前本地子线程与JavaVM解绑
* @param jvm JavaVM实例指针
* @param isNativeThread 当前线程是否为本地子线程,该入参是attachCurrentThread函数的isNativeThread出参
*
* 注意:该函数与attachCurrentThread函数配套使用
* 注意:该函数只对本地子线程有效,解绑后,可释放当前本地子线程持有的JNI同步锁(如果持有了JNI同步锁)
*/
void releaseCurrentThreadJNIEnv(JavaVM * jvm,jboolean isNativeThread){
if(isNativeThread){
jvm->DetachCurrentThread();
}
}
/**************************************** 自定义方法使用 ***********************************************/
JavaVM *g_vm = nullptr;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
//3 获取JavaVM实例
g_vm = vm;
return JNI_VERSION_1_6;
}
//线程要执行的函数
void runnable() {
jboolean result = false;
JNIEnv * env = nullptr;
//1 获取当前本地子线程、java子线程、java主线程的JNIEnv实例
env = yuyue::getCurrentThreadJNIEnv(g_vm,&result);
env->NewStringUTF("wwww");
//2 解绑当前子线程与JavaVM,释放JNIEnv,只有当前子线程是本地子线程时有效
yuyue::releaseCurrentThreadJNIEnv(g_vm,result);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_testCPlusThread(JNIEnv *env, jclass clazz){
//实例化本地线程,并运行
std::thread t(runnable);
//取消当前线程的所有join
t.detach();
// runnable();
return 1;
}
注意:AttachCurrentThread / DetachCurrentThread 函数应该只在本地子线程中使用。不应在java线程中使用。
注意:本地子线程内部调用DetachCurrentThread后,不仅会将当前本地子线程与JavaVM解绑,还会释放持有的JNI同步锁。
4 JNI层的线程同步
JNI层的代码可以运行在由C/C++创建的本地线程中,也可以运行在java线程中(与java native方法对应的C/C++函数中的逻辑就是运行在java线程中),所以JNI层的线程同步包括两种情况,分别是在本地线程中的同步 和 在java线程中的同步,明确这点后,我们继续讨论JNI层线程同步的方法。
JNI虽然没有定义在JNI层创建线程的接口,但定义了在JNI层实现同步的接口,而JNI层本来就是C/C++的语言环境,不难知道,在JNI层实现线程同步的方法有两个: 一个是用JNI标准定义的线程同步机制;一个是用C/C++定义的同步机制。
(1)利用JNI定义的同步机制
JNI定义的同步机制在本地线程 和 java线程中均有效。
1)涉及到的JNI接口
涉及到两个接口,如下:
/**
* 获取java对象 obj的同步锁
* @param obj java对象,不能为null
* @return 返回0:成功;返回负数:失败
*
* @exceptions OutOfMemoryError
*
* 注意:与MonitorExit成对调用,或者在本地子线程中调用了DetachCurrentThread,否则可能造成死锁
*/
jint MonitorEnter(jobject obj);
/**
* 释放java对象 obj的同步锁
* @param obj java对象,不能为null
* @return 返回0:成功;返回负数:失败
*
* @exceptions IllegalMonitorStateException 若当前线程不持有obj的同步锁,会报该异常
*
* 注意:如果线程没有持有obj的同步锁,不要调用该方法,否则抛上述异常
*/
jint MonitorExit(jobject obj);
2)接口使用
在JNI层定义一个函数runnable,作为线程要执行的任务,并在其中使用JNI同步。
void runnable2(const char * threadName,jobject lock){
//2 获取java实例的同步锁
if (JNI_OK != env->MonitorEnter(lock)) {
xy::printLog("yy","获取同步锁失败",ANDROID_LOG_ERROR);
}
for(int i = 0; i < 150; i++){
xy::printLog("yy",threadName,ANDROID_LOG_ERROR);
}
//3 释放java实例的同步锁
if (JNI_OK != env->MonitorExit(lock)){
xy::printLog("yy","释放同步锁失败",ANDROID_LOG_ERROR);
}
};
在本地线程中执行runnable
extern "C"
JNIEXPORT void JNICALL
Java_com_boe_jnilearn_LearnJNI_testJNISynchronise(JNIEnv *env, jclass clazz,jstring threadName) {
//1 使用jclass实例的同步锁,需要创建clazz实例的全局引用
jobject lock = env->NewGlobalRef(clazz);
std::thread t1(runnable,"t1",lock);
t1.detach();
std::thread t2(runnable,"t2",lock);
t2.detach();
// const char * c_threadName = env->GetStringUTFChars(threadName, nullptr);
// runnable1(c_threadName,lock);
}
在java线程中执行runnable
/*************************** java层 ************************/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try{
new Thread(new Runnable() {
@Override
public void run() {
LearnJNI.testJNISynchronise("t1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
LearnJNI.testJNISynchronise("t2");
}
}).start();
}catch (Throwable e){
Log.e("yy","java:" + e.getMessage());
}
}
}
/*************************** JNI层 ************************/
extern "C"
JNIEXPORT void JNICALL
Java_com_boe_jnilearn_LearnJNI_testJNISynchronise(JNIEnv *env, jclass clazz,jstring threadName) {
//1 使用jclass实例的同步锁,需要创建clazz实例的全局引用
jobject lock = env->NewGlobalRef(clazz);
// std::thread t1(runnable,"t1",lock);
// t1.detach();
// std::thread t2(runnable,"t2",lock);
// t2.detach();
const char * c_threadName = env->GetStringUTFChars(threadName, nullptr);
runnable1(c_threadName,lock);
}
注意: 如果使用JNI native方法传进来的对象锁,需要使用该对象的全局引用!否则JNI native对应的JNI函数执行结束就会回收形参,导致线程中使用的同步锁异常,进而导致程序崩溃。
(2)利用C++定义的同步机制
C++主要是利用mutex库的mutex类实现线程同步,C++的同步机制在本地线程 和 java线程中均有效。
1)涉及到的函数
mutex类一共定义了三个函数,如下:
class mutex
{
......
/**
* 作用:获取锁,若没获取到,一直阻塞
*/
void lock();
/**
* 作用:尝试获取锁,若没获取到,不会阻塞
* @return 返回true:成功;返回false:失败
*/
bool try_lock();
/**
* 作用:释放锁
*/
void unlock();
......
};
2)函数使用
将runnable函数获取和释放锁的操作替换成C++的,并去掉lock形参,如下:
//1 引入C++mutex库
#include<mutex>
//2 实例化C++互斥锁
mutex locker;
void runnable(const char * threadName,jobject lock){
//3 获取锁
locker.lock();
for(int i = 0; i < 150; i++){
xy::printLog("yy",threadName,ANDROID_LOG_ERROR);
}
//4 释放锁
locker.unlock();
};
然后分别在本地线程和java线程中执行即可看到效果。