此文章是关于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 <unistd.h>
#include <pthread.h>
#include <assert.h> void *pthread_run(void *arg) {
JNIEnv *env = NULL;
// get env ?
char *name = (char *) arg;
for (int i = ; i < ; ++i) {
//char* uuid_cstr = ...
LOGI("%s, No:%d, uuid:%s", name, i, uuid_cstr);
sleep();
} pthread_exit((void *) );
} 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的定义,明明就是四个参数的啊?

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 *) -);
} 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 = ; i < ; ++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();
} (*env)->ReleaseStringUTFChars(env, sys_uuid_jstr, sys_uuid_cstr);
(*javaVM)->DetachCurrentThread(javaVM);
pthread_exit((void *) );
}

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

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

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

为什么会找不到 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了。

Jni 线程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)的更多相关文章

  1. Android JNI 之 JNIEnv 解析

    jni.h文件 : 了解 JNI 需要配合 jni.h 文件, jni.h 是 Google NDK 中的一个文件, 位置是 $/android-ndk-r9d/platforms/android-1 ...

  2. 【Android 系统开发】Android JNI 之 JNIEnv 解析

    . jni.h文件 : 了解 JNI 需要配合 jni.h 文件, jni.h 是 Google NDK 中的一个文件, 位置是 $/android-ndk-r9d/platforms/android ...

  3. 使用FindControl("id")查找控件 返回值都是Null的问题

    做了一个通过字符串ID查找页面控件并且给页面控件赋值的功能,过程中遇到了this.FindControl("id")返回值都是Null的问题,记录一下解决办法. 问题的原因是我所要 ...

  4. 【Spring Boot-技巧】API返回值去除为NULL的字段

    简介 在前后端分离的微服务时代,后端API需要良好的规范.本篇主要将一个数据返回时的一个小技巧-- 过滤为空字段 解决痛点:将有效解决数据传输过程中的流量浪费. 组件简介 Jackson Object ...

  5. typeof运算对于null会返回“Object"

    您也许会问,为什么 typeof 运算符对于 null 值会返回 "Object".这实际上是 JavaScript 最初实现中的一个错误,然后被 ECMAScript 沿用了.现 ...

  6. mysql 对返回的值是null进行判断和重新赋值

    SELECT IFNULL(sum(c.tax_data),) ,IFNULL(SUM(c.fiscal_ret),) FROM corp_tax c WHERE c.industry_id = 使用 ...

  7. .NET Core 处理 WebAPI JSON 返回烦人的null为空

    前言 项目开发中不管是前台还是后台都会遇到烦人的null,数据库表中字段允许空值,则代码实体类中对应的字段类型为可空类型Nullable<>,如int?,DateTime?,null值字段 ...

  8. IOS开发遇到(null)与<null>轻松处理

    在ios开发中不可避免的我们会遇到服务器返回的值有空值,但是如果是nil也就算了还可能得到(null)以及<null>的返回值,该如何处理呢?(当然有的字典转模型中已处理,可以通过遍历等) ...

  9. SQL语句中=null和is null

    平时经常会遇到这两种写法:IS NOT NULL与!=NULL.也经常会遇到数据库有符合条件!=NULL的数据,但是返回为空集合.实际上,是由于对二者使用区别理解不透彻. 默认情况下,推荐使用 IS ...

随机推荐

  1. 一致性hash理解、拜占庭将军问题解读和CAP理论总结

    一致性hash理解 白话概述: 比如说存储图片,有10台服务器用来存储,对图片名进行hash(pic_name)%10得到的值就是图片存放的服务器序号.这是正常的hash算法分散图片存储.但是有一天, ...

  2. u-boot中debug的一些总结

    研究u-boot,首要搞清楚的是代码的流程,运行流程是什么样子的呢?不知道,就看log.这就要把log信息 打开.研究u-boot的文件,发现里面是很多DEBUG宏定义的打印,这个打印着怎么打开呢? ...

  3. python中不同文件中函数和类的调用

    最近在学习Python的时候,遇到了一个不同文件中类无法调用的问题,搜了很多,发现很多人针对 这个问题都说的相当含糊,让我费了好大劲才把这个东东搞明白.记录一下,权且温习. 调用分两种,一种是同种文件 ...

  4. resin远程调试配置

    1.进入resin的安装路径下的conf目录,下面有个resin.conf的文件,打开它,将下面这段配置添加进去,然后重启resin(大家应该知道如何重启吧): <jvm-arg>-Xde ...

  5. Python:SQLMap源码精读—基于错误的盲注(error-based blind)

    目标网址 http://127.0.0.1/shentou/sqli-labs-master/Less-5/?id=1 Payload的生成 <test> <title>MyS ...

  6. [教程向]__在IDEA中使用git+github实现代码的版本控制

    前言 在前面,我们对git与github有了一个简单的了解,了解到版本控制的必要性,和github远程代码仓库的一些作用.那么我们如何在IDEA重实现代码的版本控制呢. 前提 首先你要安装有git,注 ...

  7. 从零开始学 Web 之 HTML(一)认识前端

    大家好,这里是 Daotin 从零开始学 Web 系列教程.此文首发于「 Daotin的梦呓 」,欢迎大家订阅关注.在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识点,期间也会分享 ...

  8. C#效率优化(1)-- 使用泛型时避免装箱

    本想接着上一篇详解泛型接着写一篇使用泛型时需要注意的一个性能问题,但是后来想着不如将之前的详解XX系列更正为现在的效率优化XX系列,记录在工作时遇到的一些性能优化的经验和技巧,如果有什么不足,还请大家 ...

  9. SQL 必知必会·笔记<17>使用存储过程

    存储过程就是为以后使用而保存的一条或多条SQL 语句.可将其视为批文件,虽然它们的作用不仅限于批处理. 1. 存储过程的优点和缺点 存储过程的优点: 使用存储过程有三个主要的好处,即简单.安全.高性能 ...

  10. Spring Boot + Spring Cloud 构建微服务系统(十):配置中心(Spring Cloud Bus)

    技术背景 我们在上一篇讲到,Spring Boot程序只在启动的时候加载配置文件信息,这样在GIT仓库配置修改之后,虽然配置中心服务器能够读取最新的提交信息,但是配置中心客户端却不会重新读取,以至于不 ...