Jni 线程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)

此文章是关于NDK线程的第二篇理论知识笔记。主要有两个点,如下:

1.pthread_create(Too many arguements, expected 1) ?
2.线程中如何获取JNIEnv?GetEnv返回NULL?
3.FindClass返回NULL ?
首先我们在主页MainActivity的代码如下:

public class MainActivity extends Activity {

    static {
        try {
            System.loadLibrary("native-lib");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public native void nativeThreadEnvTest();

    public static String getUuid() {
        // 提供给nativeThreadEnvTest使用
        return UUID.randomUUID().toString();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) ...

}
 nativeThreadEnvTest打算实现这样一个功能:for循环调用MainActivity.getUuid方法,打印出5串不同的UUID。听上去很简单,逻辑代码如下:

#include
#include
#include

void *pthread_run(void *arg) {
    JNIEnv *env = NULL;
    // get env ?
    char *name = (char *) arg;
    for (int i = 0; i < 5; ++i) {
        //char* uuid_cstr = ...

        LOGI("%s, No:%d, uuid:%s", name, i, uuid_cstr);
        sleep(1);
    }

    pthread_exit((void *) 0);
}

JNIEXPORT void JNICALL
Java_org_zzrblog_MainActivity_nativeThreadEnvTest(JNIEnv *env, jobject thiz) {
    pthread_t tid;
    pthread_create(&tid, NULL, pthread_run, (void *) "pthread1");

    //void* reval;
    //pthread_join(tid, &reval);
}

此时第一个坑点可能就会出现了:pthread_create报出错误提示 Too many arguements, expected 1(黑人三问号)

ctrl+左键,跳转到头文件pthread.h的定义,明明就是四个参数的啊?

Jni 线程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)

rebuild,重启AS,各种大法都没解啊,怎么办? 这里给出可行科学的解决方案,在头文件添加如下宏定义就OJBK了!

#ifndef _PTHREAD_H_
#define _PTHREAD_H_

#include ...

// 添加宏定义 _Nonnull
#ifndef _Nonnull
#define _Nonnull
#endif

那么我们继续功能实现,在线程执行函数pthread_run中,想要调用MainActivity.getUuid方法,必须得有env啊。那么我们怎么获取env?可能就有大兄弟立马说:在nativeThreadEnvTest传入的env时NewGlobalRef啊,这样就可以全局使用了!这好像确实是一个解决思路,好像还蛮好使的(因为兄弟你见识得太少了)。但是!BUT!However! 严谨的说,这种做法是不可取的。为什么?引用Google官方翻译:

由于VM通常是多执行绪(Multi-threading)的执行环境。每一个执行绪在呼叫native函数时,所传递进来的JNIEnv指标值都是不同的。为了配合这种多执行绪的环境,C组件开发者在撰写native函数时,可藉由JNIEnv指标值之不同而避免执行绪的资料冲突问题,才能确保所写的native函数能安全地在Android的多执行绪VM里安全地执行。基于这个理由,当在呼叫C组件的函数时,都会将JNIEnv指标值传递到下一级函数使用。

看起来好像很抽象,似懂非懂的。但是我们必须知道:VM是多执行绪(Multi-threading) ,每个JNIEnv都是不同的!特别是在不同线程,都是独自维护各自独立的JNIEnv。 那么问题又回到最初的?怎么正确的获取线程安全的JNIEnv?

此时我们引入函数JNI_OnLoad 和 结构体JavaVM,在头文件 jni.h 有它的定义:

/*
 * Prototypes for functions exported by loadable shared libs.  These are
 * called by JNI, not provided by JNI.

 */
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);

从注释可知,JNI_OnLoad是由系统JNI回调的,并不由得开发者乱用,而且也不由JNI默认提供。不重写这个方法系统就默认进行配置。在虚拟机VM加载c组件的时候(so)就会调用组件加载接口JNI_OnLoad(),在JNI_OnLoad()函数里,就透过VM之指标而取得JNIEnv之指标值,并存入env指标变数里。

这里的JavaVM就是虚拟机VM在JNI中的表示,一个进程JVM中只有一个JavaVM对象,这个对象是线程共享的。换言之这个JavaVM是能全局安全使用的,而且也只能在JNI_OnLoad的回调进行强引用赋值。 有了这个JavaVM,我们再调用AttachCurrentThread 附加当前线程到虚拟机VM当中,并返回线程对应的JNIEnv,我们就能愉快的撸码了!

说到AttachCurrentThread,不能不提起JavaVM的另外一个接口 GetEnv,看上去GetEnv不就是获取env的方法吗?这么解释吧,只有先AttachCurrentThread到JavaVM,分配到了独立的JNIEnv之后,GetEnv第二个参数二级指针返回的env才有值。就是说JavaVM->GetEnv获取的是,此线程有效的env。JavaVM->AttachCurrentThread是向虚拟机分配线程独立的env。 所以一般在线程执行函数第一句是AttachCurrentThread,随后就能用GetEnv了。

理论知识介绍到这里,我们继续测试功能函数,现在代码应该是长这样的:

JavaVM *javaVM;

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGW("%s\n", "JNI_OnLoad startup ...");
    javaVM = vm;
    JNIEnv *env = NULL;
    jint result;

    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) == JNI_OK) {
        LOGI("Catch JNI_VERSION_1_6\n");
        result = JNI_VERSION_1_6;
    }
    else if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) == JNI_OK) {
        LOGI("Catch JNI_VERSION_1_4\n");
        result = JNI_VERSION_1_4;
    }
    else {
        LOGI("Default JNI_VERSION_1_2\n");
        result = JNI_VERSION_1_2;
    }

    assert(env != NULL);
    // 动态注册native函数 ...
    return result;
}

void *pthread_run(void *arg) {
    JNIEnv *env = NULL;
    // (*javaVM)->AttachCurrentThread(javaVM,&env,NULL)
    // (*javaVM)->GetEnv(javaVM, (void **)&env, JNI_VERSION_1_6)
    if ( (*javaVM)->AttachCurrentThread(javaVM,&env,NULL) != JNI_OK) {
        LOGE("javaVM->Env Error!\n");
        pthread_exit((void *) -1);
    }

    assert(env != NULL);

    // 自定义的类型 jclass
    jclass clazz = (*env)->FindClass(env, "org/zzrblog/MainActivity");
    jmethodID getUuid_mid = (*env)->GetStaticMethodID(env, clazz, "getUuid","()Ljava/lang/String;");

    char *name = (char *) arg;
    for (int i = 0; i < 5; ++i) {
        jobject uuid_jstr = (*env)->CallStaticObjectMethod(env, clazz, getUuid_mid);
        char* uuid_cstr = (char *) (*env)->GetStringUTFChars(env, uuid_jstr, NULL);
        LOGI("%s, No:%d, uuid:%s", name, i, uuid_cstr);
        sleep(1);
    }

    (*env)->ReleaseStringUTFChars(env, sys_uuid_jstr, sys_uuid_cstr);
    (*javaVM)->DetachCurrentThread(javaVM);
    pthread_exit((void *) 0);
}

我们在JNI_OnLoad函数全局引用JavaVM对象,然后就是模板代码了,告诉系统虚拟机用哪个版本的JNI。此时调用JavaVM->GetEnv获取的env是主线程的。所以我们能获取成功。

然后我们进入线程执行函数,使用AttachCurrentThread请求分配当前线程安全的env,之后我们使用FindClass / GetStaticMethodID / CallStaticObjectMethod 等JNI API进行Java层的MainActivity.getUuid 静态方法的调用。

一切看着都是那么顺利,然后运行demo瞬间报错(奸笑.jpg)一堆红通通的错误啊!

Jni 线程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)

为什么会找不到 org.zzrblog.MainActivity?此问题更好的体现了JNIEnv的线程独立性问题了!如果FindClass用的是主线程env就不会报错了。如果FindClass的是系统的UUID类也不会报错了。但是现实生活没有那么多如果!问题的原因就是只有主线程的env才有包含我们自定义(自己开发)的类类型,而 AttachCurrentThread的线程安全env只加载了系统的类类型,并不包含自定义的类类型。
所以问题的原因找到了,怎么解?既然env不是线程安全,不能直接引用。那么我们可以引用其他线程共享的调用对象啊,再通过GetObjectClass获取jclass。不明白的同学看如下代码:

jobject jMainActivity;

JNIEXPORT void JNICALL
Java_org_zzrblog_MainActivity_nativeThreadEnvTest(JNIEnv *env, jobject thiz) {
    if(jMainActivity == NULL) {
        //调用对象,创建全局引用
        jMainActivity = (*env)->NewGlobalRef(env, thiz);
    }

    pthread_t tid;
    pthread_create(&tid, NULL, pthread_run, (void *) "pthread1");
    //void* reval;
    //pthread_join(tid, &reval);
}

现在知道native方法的 static 和 非static的时候,传入的第二个参数的意义和真实的用处了吧?

非static的时候,传入jobject类型的thiz,就相当于MainActivity.this了。

staic的时候,传入jcalss类型的clazz,就相当于MainActivity.class了。

Original: https://www.cnblogs.com/zhujiabin/p/10605792.html
Author: 星辰之力
Title: Jni 线程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/538842/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 几个不错的可视化库

    Echarts highcharts 『注:本文来自博客园”小溪的博客”,若非声明均为原创内容,请勿用于商业用途,转载请注明出处http://www.cnb…

    Java 2023年6月16日
    066
  • 数据结构中点链表的插入操作

    Status listinsert(Linklist &L,int i,ElemType e) //bool vs status :在函数需要有返回值的时候,既可以使用bo…

    Java 2023年6月5日
    076
  • Java RMI详解

    RMI:远程方法调用(Remote Method Invocation)。能够让在某个java虚拟机上的对象像调用本地对象一样调用另一个java 虚拟机中的对象上的方法。 RMI远…

    Java 2023年5月29日
    086
  • How to code like a pro in 2022 and avoid If-Else

    高级开发人员如何编写代码: var input = "Dog"; var map = new Dictionary<string, string> …

    Java 2023年6月15日
    077
  • 厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证!

    小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么锅都想甩给他,啊,不,一不小心怎么把心里话全说出来了呢?重来! 小二是新来的实习生,作为技术 leader,我还…

    Java 2023年6月9日
    098
  • MySQL基础篇(一)

    本文主要内容为MySQL的基础语句以及正则表达式等内容。本文操作的数据库内容存在个人github:https://github.com/YuanGao-1/blog_demo.gi…

    Java 2023年6月7日
    089
  • C#中的线程之Abort陷阱

    1.简介 C#中通常使用线程类Thread来进行线程的创建与调度,博主在本文中将分享多年C#开发中遇到的Thread使用陷阱。 Thread调度其实官方文档已经说明很详细了。本文只…

    Java 2023年5月29日
    085
  • HTTP Study

    定义:在两点之间传输文本,视频,图片等超文本数据的协议和规范 HTTP风险 通信使用明文,https通过信息加密(混合加密)解决 无法验证报文的完整性,https通过校验机制(摘要…

    Java 2023年6月8日
    079
  • springboot各层作用

    搞懂springboot各层作用 总结springboot项目流程 Springboot项目分为以下几个层: controller层:控制层,负责前后端交互,接收前端发送的请求,然…

    Java 2023年6月7日
    071
  • C C++结构体四种方式

    第一种语法表示 struct &#x7ED3;&#x6784;&#x4F53;&#x540D;&#x79F0; {&#xA0; &a…

    Java 2023年6月16日
    067
  • 5-多线程

    一、创建多线程的四种方式 1.方式一:继承Thread类的方式 创建一个继承于Thread类的子类 重写Thread类的run() –> 将此线程执行的操作声明在…

    Java 2023年6月7日
    0101
  • 五月,你好啊!

    五月,你好啊! 当我连续出现在深夜,就代表… 最近要学得东西变多了许多,今天是五四青年节,吾辈青年,请继续前进于中国强国道路上,不断努力奋斗,学习专业知识与个人技能专长…

    Java 2023年6月5日
    073
  • 【翻译】Spring Security抛弃了WebSecurityConfigurerAdapter

    作者:ELEFTHERIA STEIN-KOUSATHANA 发表日期:2022年2月21日 在Spring Security 5.7.0-M2,我们弃用了 WebSecurity…

    Java 2023年6月6日
    097
  • python中三目运算符与条件判断语句

    对java而言,存在三目运算符如: [result] = [conditional expression] ? [expression1]: [expression2] 可以达到 …

    Java 2023年6月9日
    076
  • 云原生系列2 部署你的第一个k8s应用

    云原生的概念和理论体系非常的完备,but talk is cheap , show me the code ! 但是作为一名程序员,能动手的咱绝对不多BB,虽然talk并不chea…

    Java 2023年6月8日
    079
  • centos7更改中文

    这是在CentOS7中设置,CentOS6的是在 .etc/sysconfig/i18n 配置文件下。在root用户下操作,使用 locale 命令查看语言环境,看到 LANG=e…

    Java 2023年6月16日
    067
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球