细说JNI与NDK专题目录:
细说JNI与NDK(一) 初体验
细说JNI与NDK(二)基本操作)
细说JNI与NDK(三)ndk 配置说明
细说JNI与NDK(四)动态和静态注册
细说JNI与NDK(五)JNI 线程
细说JNI与NDK(六)静态缓存,异常捕获,内置函数
细说JNI与NDK(七)Parcel底层JNI思想与OpenCV简单对比
JNI 线程
我们写业务时候,可能会有耗时操作,放在JNI子线程,也会在JNI切换线程,调用Java端代码。所有这个时候,在JNI 线程调用Java, Android 在操作UI一定判断线程是否主线程
/**
* 下面是 被native代码调用的 Java方法
* 第二部分 JNI线程
*/
public void updateActivityUI() {
if (Looper.getMainLooper() == Looper.myLooper()) {
new AlertDialog.Builder(JavaJNIActivity.this)
.setTitle("UI")
.setMessage("updateActivityUI Activity UI ...")
.setPositiveButton("Activity UI知道了", null)
.show();
} else {
Log.d(TAG, "updateActivityUI 所属于子线程,只能打印日志了..");
runOnUiThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(JavaJNIActivity.this)
.setTitle("updateActivityUI")
.setMessage("所属于子线程,只能打印日志了..")
.setPositiveButton("Activity UI知道了", null)
.show();
}
});
}
}
}
回顾一下前面的C++线程在JNI中的操作
pthread_t pid;
pthread_create(&pid,nullptr,thread_task_action,nullptr);
利用pthread 创建线程,传入pid,thread_task_action 指针函数。通常我们需要传参数,但是这个参数(第三个参数)是有一些需要注意的,下面详细说明,先看下整体
JNI 线程切换
如场景:有下载任务,下载完成,必须告诉Android UI所有需要在子线程,调用UI的情况。
struct MyContext{
JNIEnv *jniEnv = nullptr;
jobject instance = nullptr;
};
extern "C"
JNIEXPORT void JNICALL
Java_top_zcwfeng_jni_JavaJNIActivity_naitveThread(JNIEnv *env, jobject thiz) {
// 创建线程
// pthread_t pid;
//pthread_create(&pid,nullptr,thread_task_action,nullptr);
MyContext * context = new MyContext;
context->jniEnv = env;
① 报错设计
context->instance = thiz;
//② 修正错误,提升全局引用
// context->instance = env->NewGlobalRef(thiz);
pthread_t pid;
pthread_create(&pid,nullptr,thread_task_action,context);
pthread_join(pid,nullptr);
}
void * thread_task_action(void * pVoid){
// 有类似场景:如,下载任务,下载完成,下载失败,等等,需要更新UI,要告知Android UI线程情况
// 需要用到JNIEnv *env,jobject thiz,跨函数,跨线程有问题
MyContext * context = static_cast<MyContext *>(pVoid);
jclass call_class = context->jniEnv->FindClass(class_name);
context->jniEnv->GetObjectClass(context->instance);
return nullptr;
}
- 错误一:代码中设计了一个问题,①和② 局部引用①跨函数jobject是有问题的
- 错误二: pthread_create时候,thread_task_action函数接收的值,会有多个,这个时候我们封装参数到结构体中MyContext,这个是通用的做法。
但是如下写法会出现问题:
xxxxxxxxxxxx错误代码xxxxxxxxxxxxxxx
// 需要用到JNIEnv *env,jobject thiz
MyContext * context = static_cast<MyContext *>(pVoid);
jclass call_class = context->jniEnv->FindClass(class_name);
context->jniEnv->GetObjectClass(context->instance);
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
原因是jniEnv,jobject 跨线程和跨函数是有限制的,详细规则,下文仔细描述。上面是个错误实例代码,两处错误
跨线程和跨函数需要十分注意
JNIEnv 不能
跨线程,可以
在不同函数中使用
「解决方式:利用JavaVM全局跨进程特性,附加线程。定义新的JNIEnv,附加到当前线程,然后解除临时JNIEnv的绑定」
jobject 不能
跨越函数,不能
跨越线程
「jobject 默认是局部引用,提升全局引用,可以解决」
JavaVM 能够跨越
线程,能够跨越
函数
「JavaVM-》JVM是绑定进程的。是全局的可以跨越线程的」
JNIEnv -》::javaVM-》jint attachResult = AttachCurrentThread(&jniEnv,nullptr); 构建出新的JNIEnv,是一个二级指针.
结束需要解除绑定::javaVM->DetachCurrentThread();
解决上面2个错误,完整代码
➜ 调用 Java-> updateActivityUI √ 代码
//TODO:JNI 线程
void * thread_task_action(void * pVoid){
// 有类似场景:如,下载任务,下载完成,下载失败,等等,需要更新UI,要告知Android UI线程情况
MyContext * context = static_cast<MyContext *>(pVoid);
// 需要用到JNIEnv *env,jobject thiz,跨函数,跨线程有问题
/*
jclass call_class = context->jniEnv->FindClass(class_name);
context->jniEnv->GetObjectClass(context->instance);*/
//TODO 验证(Android 进程绑定只有一个JavaVM,是全局的,可以跨越线程的)
JNIEnv *env = nullptr;
jint attachResult = ::javaVm->AttachCurrentThread(&env,nullptr);
if(attachResult != JNI_OK){
return 0;// attach current thread failed
}
// 1 拿到jclass
jclass call_class = env->GetObjectClass(context->instance);
// 2 拿到方法
jmethodID jmethodId = env->GetMethodID(call_class,"updateActivityUI","()V");
// 3
env->CallVoidMethod(context->instance,jmethodId);
::javaVm->DetachCurrentThread();
LOGE("C++ 异步线程调用 Java-> updateActivityUI Success! ")
return nullptr;
}
extern "C"
JNIEXPORT void JNICALL
Java_top_zcwfeng_jni_JavaJNIActivity_naitveThread(JNIEnv *env, jobject thiz) {
// 创建线程
// pthread_t pid;
// pthread_create(&pid,nullptr,thread_task_action,nullptr);
MyContext * context = new MyContext;
context->jniEnv = env;
//① 报错设计
//context->instance = thiz;
//② 修正错误,提升全局引用
context->instance = env->NewGlobalRef(thiz);
pthread_t pid;
pthread_create(&pid,nullptr,thread_task_action,context);
pthread_join(pid,nullptr);
}
- 提升MyContext中jobject 为全局引用
- 跨线程,利用JavaVM绑定进程特性,新建JNIEnv 并绑定当前线程,干完活,detach解绑。
验证JavaVm 地址,证明JavaVM,jobject,JNIEnv 问题
➜ Java 代码和调用
public native void nativeFun1();
public native void nativeFun2();
public static native void staticFun3();
public static native void staticFun4();
调用1:
nativeFun1();
nativeFun2();
staticFun3();
new Thread() {
@Override
public void run() {
super.run();
staticFun4();
}
}.start();
调用2:
startActivity(new Intent(this, JNIActivityThreadTest.class));
class JNIActivityThreadTest : AppCompatActivity() {
external fun nativeFun5()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_j_n_i_thread_test)
nativeFun5();
}
}
- 两个不同的native 方法比较(nativeFun1,nativeFun2)
- class 方法比较(nativeFun3,nativeFun4)
- java 子线程调用 native 的子线程
- 跨Activity(nativeFun5) 比较 JavaVm
1.JavaVm 比较
2.JNIEnv 绑定线程,不能跨越
3.native 子线程env和java子线程env不一样
- jobject Activity 不同所以Jobject不同,谁调用就是谁
看下结果
E/native_zcw: 当前函数env地址 nativeFun1-->0xe5619400, jvm地址:0xe55867a0, jobject 地址:0xfff4de1c, JNI_OnLoad的jvm地址:0xe55867a0
E/native_zcw: 当前函数env地址 nativeFun2-->0xe5619400, jvm地址:0xe55867a0, jobject 地址:0xfff4de1c, JNI_OnLoad的jvm地址:0xe55867a0
E/native_zcw: 当前函数env地址 nativeFun3-->0xe5619400, jvm地址:0xe55867a0, jclass 地址:0xfff4de3c, JNI_OnLoad的jvm地址:0xe55867a0
E/native_zcw: 当前函数env地址 nativeFun4 Java 子线程-->0xc6113700, jvm地址:0xe55867a0, jclass 地址:0xc4f040ec, JNI_OnLoad的jvm地址:0xe55867a0
E/native_zcw: native Jni 子线程 env地址 run-->0xc6113d80 jvm地址:0xe55867a0 ~~~~~~~
E/native_zcw: 当前函数env地址 nativeFun5 跨Activity -->0xe5619400, jvm地址:0xe55867a0, jobject 地址:0xfff4da4c, JNI_OnLoad的jvm地址:0xe55867a0
首先可以看到全局所有JavaVM的地址是不变的--》0xe55867a0
nativeFun1,nativeFun2,nativeFun3 对比 env 地址也是不变---》 0xe5619400
env地址 nativeFun4---》子线程原因不同-->0xc6113700
jclass不同
nativeFun3,nativeFun4 也是因为子线程原因
jobject nativeFun1,nativeFun2 相同
可以看到跨Activity
env 地址相同,---》 0xe5619400,JavaVM 地址都是一致-》0xe55867a0。但是jobject 地址是不同的。
证明了上面JavaVM是进程唯一。
网友评论