【问题标题】:What is the best way to save JNIEnv*保存 JNIEnv* 的最佳方法是什么
【发布时间】:2015-07-13 14:19:21
【问题描述】:

我有一个带有 JNI 的 Android 项目。在实现侦听器类的 CPP 文件中,有一个回调 x() 。当调用 x() 函数时,我想调用 java 类中的另一个函数。但是,为了调用该 java 函数,我需要访问 JNIEnv*。

我知道在回调的同一个cpp文件中,有一个函数:

static jboolean init (JNIEnv* env, jobject obj) {...}

当调用init(..) 时,我应该将JNIEnv* 作为成员变量保存在cpp 文件中吗?并在回调发生时使用它?

对不起,我是 JNI 的初学者。

【问题讨论】:

    标签: android android-ndk java-native-interface jnienv


    【解决方案1】:

    @Michael,很好地概述了如何通过缓存 JVM 来最好地检索 JNI。 对于那些不想使用 pthread (或者因为你在 Windows 系统上),并且你使用的是 c++ 11 或更高版本的人,那么 thread_local 存储是要走的路。

    下面是一个粗略的例子,说明如何实现一个包装器方法,该方法可以正确附加到线程并在线程退出时自动清理

    JNIEnv* JNIThreadHelper::GetJniEnv() {
    
        // This method might have been called from a different thread than the one that created
        // this handler. Check to make sure that the JNI is attached and if not attach it to the 
        // new thread.
    
        // double check it's all ok
        int nEnvStat = m_pJvm->GetEnv(reinterpret_cast<void**>(&m_pJniEnv), JNI_VERSION_1_6);
    
        if (nEnvStat == JNI_EDETACHED) {
    
            std::cout << "GetEnv: not attached. Attempting to attach" << std::endl;
    
            JavaVMAttachArgs args;
            args.version = JNI_VERSION_1_6; // choose your JNI version
            args.name = NULL; // you might want to give the java thread a name
            args.group = NULL; // you might want to assign the java thread to a ThreadGroup
    
            if (m_pJvm->AttachCurrentThread(&m_pJniEnv, &args) != 0) {
                std::cout << "Failed to attach" << std::endl;
                return nullptr;
            }
    
            thread_local struct DetachJniOnExit {
                ~DetachJniOnExit() {
                    m_pJvm->DetachCurrentThread();
                }
            };
    
    
            m_bIsAttachedOnAThread = true;
    
        }
        else if (nEnvStat == JNI_OK) {
            //
        }
        else if (nEnvStat == JNI_EVERSION) {
    
            std::cout << "GetEnv: version not supported" << std::endl;
            return nullptr;
        }
    
    
        return m_pJniEnv;
    }
    

    【讨论】:

    • 请注意,这个问题已经有一个公认的答案。请edit您的答案,以确保它改进了此问题中已经存在的其他答案。
    【解决方案2】:

    缓存JNIEnv* 并不是一个特别好的主意,因为您不能在多个线程中使用相同的JNIEnv*,甚至可能无法将它用于同一线程上的多个本机调用(请参阅@ 987654321@)

    编写一个获取JNIEnv* 并在必要时将当前线程附加到VM 的函数并不太难:

    bool GetJniEnv(JavaVM *vm, JNIEnv **env) {
        bool did_attach_thread = false;
        *env = nullptr;
        // Check if the current thread is attached to the VM
        auto get_env_result = vm->GetEnv((void**)env, JNI_VERSION_1_6);
        if (get_env_result == JNI_EDETACHED) {
            if (vm->AttachCurrentThread(env, NULL) == JNI_OK) {
                did_attach_thread = true;
            } else {
                // Failed to attach thread. Throw an exception if you want to.
            }
        } else if (get_env_result == JNI_EVERSION) {
            // Unsupported JNI version. Throw an exception if you want to.
        }
        return did_attach_thread;
    }
    

    你会使用它的方式是:

    JNIEnv *env;
    bool did_attach = GetJniEnv(vm, &env);
    // Use env...
    // ...
    if (did_attach) {
       vm->DetachCurrentThread();
    }
    

    您可以将其包装在一个类中,该类在构造时附加,在破坏时分离,RAII 风格:

    class ScopedEnv {
    public:
        ScopedEnv() : attached_to_vm_(false) {
            attached_to_vm_ = GetJniEnv(g_vm, &env_);  // g_vm is a global
        }
    
        ScopedEnv(const ScopedEnv&) = delete;
        ScopedEnv& operator=(const ScopedEnv&) = delete;
    
        virtual ~ScopedEnv() {
            if (attached_to_vm_) {
                g_vm->DetachCurrentThread();
                attached_to_vm_ = false;
            }
        }
    
        JNIEnv *GetEnv() const { return env_; }
    
    private:
        bool attached_to_env_;
        JNIEnv *env_;
    };
    
    // Usage:
    
    {
        ScopedEnv scoped_env;
        scoped_env.GetEnv()->SomeJniFunction();
    }
    // scoped_env falls out of scope, the thread is automatically detached if necessary
    

    编辑: 有时您可能有一个运行时间较长的本机线程,在多个场合需要JNIEnv*。在这种情况下,您可能希望避免不断地将线程附加到 JVM 或从 JVM 分离,但您仍然需要确保在线程销毁时分离线程。

    您可以通过只附加一次线程然后将其保持附加状态来完成此操作,并使用pthread_key_createpthread_setspecific 设置线程销毁回调来处理调用DetachCurrentThread

    /**
     * Get a JNIEnv* valid for this thread, regardless of whether
     * we're on a native thread or a Java thread.
     * If the calling thread is not currently attached to the JVM
     * it will be attached, and then automatically detached when the
     * thread is destroyed.
     */   
    JNIEnv *GetJniEnv() {
        JNIEnv *env = nullptr;
        // We still call GetEnv first to detect if the thread already
        // is attached. This is done to avoid setting up a DetachCurrentThread
        // call on a Java thread.
    
        // g_vm is a global.
        auto get_env_result = g_vm->GetEnv((void**)&env, JNI_VERSION_1_6);
        if (get_env_result == JNI_EDETACHED) {
            if (g_vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
                DeferThreadDetach(env);
            } else {
                // Failed to attach thread. Throw an exception if you want to.
            }
        } else if (get_env_result == JNI_EVERSION) {
            // Unsupported JNI version. Throw an exception if you want to.
        }
        return env;
    }
    
    void DeferThreadDetach(JNIEnv *env) {
        static pthread_key_t thread_key;
    
        // Set up a Thread Specific Data key, and a callback that
        // will be executed when a thread is destroyed.
        // This is only done once, across all threads, and the value
        // associated with the key for any given thread will initially
        // be NULL.
        static auto run_once = [] {
            const auto err = pthread_key_create(&thread_key, [] (void *ts_env) {
                if (ts_env) {
                    g_vm->DetachCurrentThread();
                }
            });
            if (err) {
                // Failed to create TSD key. Throw an exception if you want to.
            }
            return 0;
        }();
    
        // For the callback to actually be executed when a thread exits
        // we need to associate a non-NULL value with the key on that thread.
        // We can use the JNIEnv* as that value.
        const auto ts_env = pthread_getspecific(thread_key);
        if (!ts_env) {
            if (pthread_setspecific(thread_key, env)) {
                // Failed to set thread-specific value for key. Throw an exception if you want to.
            }
        }
    }
    

    如果您可以使用__cxa_thread_atexit,您也许可以使用在其析构函数中调用DetachCurrentThreadthread_local 对象来完成同样的事情。

    【讨论】:

    • 嗨@Michael,感谢您使用 TLS 在退出时自动分离线程的巧妙方法——分离代码不拥有的线程(例如,某些框架线程)也很有用。
    • > 缓存 JNIEnv* 并不是一个特别好的主意,[...] 甚至可能无法将它用于同一线程上的多个本机调用 —我没有看到链接的文章表明 JNI 接口指针可能会因给定线程而改变。 specification 表示“当 VM 从同一个 Java 线程多次调用本机方法时,它保证将相同的接口指针传递给本机方法”。我认为 Invocation API 没有什么不同。
    • @DmitryTimofeev:“我没有看到链接的文章表明 JNI 接口指针可能会针对给定线程而改变”。这指的是谷歌的措辞“如果下一次本地调用发生在同一个线程上,它们可能有效”,即他们选择“可能是”而不是“将是” .
    • 知道了,谢谢!如果它们的实现不符合规范,那就太奇怪了。无论如何,只使用JavaVM#GetEnv(如果代码期望线程被附加)或JavaVM#AttachCurrentThread来获取有效指针肯定更可靠。
    • 实际上,当我想到它时,我无法理解这句话——如果 JNIEnv 是通过AttachCurrentThread 获得的(JNIEnv 是它的 output 参数),那么调用者唯一合理的期望是他们可以在同一个线程中使用它,只要它保持连接状态(即,直到这个线程调用DetachCurrentThread)。
    猜你喜欢
    • 2015-07-02
    • 2011-04-04
    • 2018-03-17
    • 2018-08-14
    • 2021-11-02
    • 2018-06-16
    相关资源
    最近更新 更多