细说JNI与NDK(五)JNI 线程

作者: zcwfeng | 来源:发表于2021-04-21 00:17 被阅读0次

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

    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 线程

        本文链接:https://www.haomeiwen.com/subject/evgvlltx.html