细说JNI与NDK(五)JNI 线程

细说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(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(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(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不一样

  1. 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是进程唯一。

你可能感兴趣的:(细说JNI与NDK(五)JNI 线程)