Android NDK开发(十三):JNI函数接口详解—线程相关

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线程中执行即可看到效果。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android NDK开发是指利用NDK(Native Development Kit)将C/C++开发的代码编译成so库,然后通过JNIJava Native Interface)让Java程序调用。在Android开发中,默认使用的是Android SDK进行Java语言开发,而对于一些需要使用C/C++的高性能计算、底层操作或跨平台需求的场景,可以使用NDK进行开发。 在Android Studio中进行NDK开发相对于Eclipse来说更加方便,特别是在Android Studio 3.0及以上版本中,配置更加简化,并引入了CMake等工具,使得开发更加便捷。首先要进行NDK开发,需要配置环境,包括导入NDK、LLDB和CMake等工具。可以通过打开Android Studio的SDK Manager,选择SDK Tools,在其中选中相应的工具进行导入。 在项目的build.gradle文件中,可以配置一些NDK相关的参数,例如编译版本、ABI过滤器等。其中,可以通过externalNativeBuild配置CMake的相关设置,包括CMakeLists.txt文件的路径和版本号。此外,在sourceSets.main中还可以设置jniLibs.srcDirs,指定so库的位置。 在进行NDK开发时,可以在jni文件夹中编写C/C++代码,并通过JNI调用相关函数。通过JNI接口,可以实现Java与C/C++之间的相互调用,从而实现跨语言开发。 综上所述,Android NDK开发是指利用NDK将C/C++开发的代码编译成so库,并通过JNI实现与Java的相互调用。在Android Studio中进行NDK开发相对方便,可以通过配置环境和相应的参数来进行开发。<span class="em">1</span><span class="em">2</span><span class="em">3</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值