无法执行JavaVM->DetachCurrentThread(): "尝试在仍在运行代码时分离"

9
我有一个使用NDK的Android应用程序,它是一个普通的Android Java应用程序,具有常规UI和C++核心。在核心代码中,我需要调用Java方法,这意味着我需要为该线程获取一个JNIEnv*,这又意味着我需要调用JavaVM-> AttachCurrentThread()来获取有效的env。
以前,我只是在做AttachCurrentThread并没有去解除绑定。在Dalvik中运行很好,但是ART会在一个调用了AttachCurrentThread的线程退出而没有调用DetachCurrentThread时立即终止应用程序。所以我阅读了JNI参考文献,确实说我必须调用DetachCurrentThread。但是当我这样做时,ART会用以下信息中止应用程序:
尝试在仍在运行代码时取消分离
那么问题出在哪里?怎样正确地调用DetachCurrentThread?

为什么不直接使用 JavaVM->GetEnv(void** penv, jint version); 来获取 JNIEnv - alijandro
3
如果当前线程未附加到虚拟机,GetEnv将失败(它会返回JNI_EDETACHED并给你一个NULL JNIEnv*)。然而,先调用GetEnv似乎是个好主意,只有在它返回JNI_EDETACHED时才调用AttachCurrentThread - Michael
@Michael:这似乎是个好主意,谢谢。然而,JNI引用明确指出,在已经附加的线程上调用AttachCurrentThread是无操作的。因此,我认为AttachCurrentThread已经实现了你所建议的功能。 - Violet Giraffe
3
在某些情况下,知道AttachCurrentThread是否实际上已经将线程附加是很重要的,这样你就不会在无操作附加后调用DetachCurrentThread。我最终编写了一个C++类,在构造时从GetEnvAttachCurrentThread获取JNIEnv*,并且仅在构造函数必须调用AttachCurrentThread时在析构函数中调用DetachCurrentThread。这样,我可以声明这些对象,使用它们的JNIEnv*,并且当对象超出范围时自动分离线程。 - Michael
@Michael,你说得对!那可能是我的错误——多次调用detach。我认为这不应该发生,因为每次请求JNIEnv*时我都会调用attach,并且当我完成后我会调用detach(通过C++ RAII风格的包装器来实现)。但我会尝试你的方法,这肯定更安全、更干净。 - Violet Giraffe
2个回答

8
Dalvik也会在线程退出而未分离时中止。这是通过pthread key实现的——请参见Thread.cpp中的threadExitCheck()
除非调用堆栈为空,否则线程可能不会分离。其背后的原因是确保任何资源(如监视器锁定(即synchronized语句))在堆栈展开时被正确释放。
按照规范定义,第二个及随后的附加调用是低成本的无操作。没有引用计数,所以无论发生了多少次附加,分离始终会分离。一种解决方案是添加自己的引用计数包装器。
另一种方法是每次附加和分离。在某些回调中,应用程序框架使用此方法。这不是一个故意的选择,而是将Java源代码包装在主要由C++开发的代码周围的副作用,并尝试将功能塞入其中。如果查看SurfaceTexture.cpp,特别是JNISurfaceTextureContext::onFrameAvailable(),您可以看到当SurfaceTexture需要调用Java语言回调函数时,它将附加线程,调用回调,然后如果线程刚刚被附加,它将立即分离它。通过调用GetEnv设置“needsDetach”标志以查看线程是否先前已附加。
从性能上来说,这并不是一个很好的事情,因为每个附加都需要分配一个Thread对象并进行一些内部VM管理,但它确实产生了正确的行为。

那么 AttachCurrentThread 在性能方面不是免费的吗?我可能想要为每个线程缓存 JNIEnv* 并在后续调用中查找它,而不是每次都这样做。但问题在于,我不知道何时分离... 这需要一些思考。 - Violet Giraffe
1
最初的附加操作是不免费的。一旦附加,任何后续的附加调用基本上都是免费的。知道何时分离的困难就是为什么SurfaceTexture代码将根据需要每次执行附加/分离操作的原因。我认为问题出在stagefright媒体播放器库从开始的本地线程中发布视频帧... SurfaceTexture无法看到线程的生命周期,因此无法事先执行附加操作,并且在线程退出之前没有机会进行分离操作。在30fps的情况下,性能下降是可以接受的,因此没有尝试修改libstagefright。 - fadden
这个答案救了我的一天。你提供的链接是很好的例子。非常感谢。 - Martin
我进行了更深入的挖掘,并发现了一个错误 https://issuetracker.google.com/issues/37079050,该错误于2016年1月被报告,涉及SurfaceTexture的onFrameAvailable监听器性能差的问题。 - tmm1
@tmm1:如果执行ST回调的线程尚未附加到VM,则需要进行附加/分离。您应该发布一个新问题,解释您正在尝试以更高的水平做什么。采用不同的方法可能会完全避免这种情况。 - fadden
显示剩余2条评论

6

我将尝试以直接实用的方式(包含示例代码,不使用类)回答这个与Android相关的问题,为那些偶然遇到这个错误且在某个操作系统或框架更新(Qt?)后出现了这个错误和信息的开发人员提供帮助。

    JNIEXPORT void Java_com_package_class_function(JNIEnv* env.... {

        JavaVM* jvm;
        env->GetJavaVM(&jvm);

        JNIEnv* myNewEnv; // as the code to run might be in a different thread (connections to signals for example) we will have a 'new one'
        JavaVMAttachArgs jvmArgs;
        jvmArgs.version = JNI_VERSION_1_6;

        int attachedHere = 0; // know if detaching at the end is necessary
        jint res = jvm->GetEnv((void**)&myNewEnv, JNI_VERSION_1_6); // checks if current env needs attaching or it is already attached
        if (JNI_EDETACHED == res) {
            // Supported but not attached yet, needs to call AttachCurrentThread
            res = jvm->AttachCurrentThread(reinterpret_cast<JNIEnv **>(&myNewEnv), &jvmArgs);
            if (JNI_OK == res) {
                attachedHere = 1;
            } else {
                // Failed to attach, cancel
                return;
            }
        } else if (JNI_OK == res) {
            // Current thread already attached, do not attach 'again' (just to save the attachedHere flag)
            // We make sure to keep attachedHere = 0
        } else {
            // JNI_EVERSION, specified version is not supported cancel this..
            return;
        }

        // Execute code using myNewEnv
        // ...

        if (attachedHere) { // Key check
            jvm->DetachCurrentThread(); // Done only when attachment was done here
        }
    }

看了 The Invocation API docs 中的 GetEnv 部分后,一切都变得清晰明了:

返回值: 如果当前线程未连接到虚拟机,则将 *env 设置为 NULL,并返回 JNI_EDETACHED。如果指定版本不受支持,则将 *env 设置为 NULL,并返回 JNI_EVERSION。否则,将 *env 设置为适当的接口,并返回 JNI_OK。

感谢以下人士: - 这个问题Getting error "attempting to detach while still running code" when calling JavaVm->DetachCurrentThread 的示例清楚地表明每次都需要仔细检查(即使在调用 detach 前没有这样做)。 - @Michael 在这个问题的评论中清楚地指出了不要调用 detach。 - @fadden 所说的:“没有引用计数,因此无论发生了多少次连接,detach 都会分离。”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接