【问题标题】:Why does the same native thread appear to call methods from different Java threads?为什么同一个本机线程似乎从不同的 Java 线程调用方法?
【发布时间】:2020-04-20 12:47:22
【问题描述】:

我正在尝试为我的活动动态设置屏幕亮度。起初,在我进入ALooper 循环之前,我很容易通过JNIEnv 调用CallVoidMethod 和公司来做到这一点。但经过 65 次循环迭代后,我开始不断收到如下异常:

android.view.ViewRootImpl$CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能接触其视图。

让我感到奇怪的是,我从同一个本机线程执行 JNIEnv 调用,但调用只是在多次尝试后才开始失败。

这里发生了什么?如何确保从正确的 Java 线程调用 Java 方法?

这是简化的示例代码:

#include <cmath>
#include <stdexcept>
#include <jni.h>

#include <android/log.h>
#include <android_native_app_glue.h>

#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG,"color-picker", __VA_ARGS__))

class JNIEnvGetter
{
    JavaVM* javaVM = nullptr;
    JNIEnv* jniEnv = nullptr;
    bool threadAttached = false;
public:
    JNIEnvGetter(ANativeActivity* activity)
        : javaVM(activity->vm)
    {
        // Get JNIEnv from javaVM using GetEnv to test whether
        // thread is attached or not to the VM. If not, attach it
        // (and note that it will need to be detached at the end
        //  of the function).
        switch (javaVM->GetEnv((void**)&jniEnv, JNI_VERSION_1_6))
        {
        case JNI_OK:
            LOGD("No need to attach thread");
            break;
        case JNI_EDETACHED:
        {
            const auto result = javaVM->AttachCurrentThread(&jniEnv, nullptr);
            if(result == JNI_ERR)
                throw std::runtime_error("Could not attach current thread");
            LOGD("Thread attached");
            threadAttached = true;
            break;
        }
        case JNI_EVERSION:
            throw std::runtime_error("Invalid java version");
        }
    }
    JNIEnv* env() { return jniEnv; }
    ~JNIEnvGetter()
    {
        if(threadAttached)
          javaVM->DetachCurrentThread();
    }
};

void setBrightness(JNIEnv* env, ANativeActivity* activity, const float screenBrightness)
{
    LOGD("setBrightness()");
    const jclass NativeActivity = env->FindClass("android/app/NativeActivity");
    const jclass Window = env->FindClass("android/view/Window");

    const jmethodID getWindow = env->GetMethodID(NativeActivity, "getWindow",
                                 "()Landroid/view/Window;");
    const jmethodID getAttributes = env->GetMethodID(Window, "getAttributes",
                                     "()Landroid/view/WindowManager$LayoutParams;");
    const jmethodID setAttributes = env->GetMethodID(Window, "setAttributes",
                                     "(Landroid/view/WindowManager$LayoutParams;)V");

    const jobject window = env->CallObjectMethod(activity->clazz, getWindow);
    const jobject attrs = env->CallObjectMethod(window, getAttributes);
    const jclass LayoutParams = env->GetObjectClass(attrs);

    const jfieldID screenBrightnessID = env->GetFieldID(LayoutParams, "screenBrightness", "F");
    env->SetFloatField(attrs, screenBrightnessID, screenBrightness);
    env->CallVoidMethod(window, setAttributes, attrs);
    if(env->ExceptionCheck())
    {
        LOGD("Exception detected");
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
    else
    {
        static int count=0;
        LOGD("Brightness set successfully %d times", ++count);
    }

    env->DeleteLocalRef(attrs);
    env->DeleteLocalRef(window);
}

void android_main(struct android_app* state)
{
    JNIEnvGetter jeg(state->activity);
    const auto env=jeg.env();
    setBrightness(env, state->activity, 1); // works fine

    for(float x=0;;x+=0.001)
    {
        int events;
        struct android_poll_source* source;
        while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0)
        {
            if (source)
                source->process(state, source);

            if (state->destroyRequested != 0)
                return;
        }

        setBrightness(env, state->activity, std::cos(x)); // gets exception
    }
}

来自adb logcat的相关输出:

04-20 15:34:45.778 12468 12487 D color-picker: Thread attached
04-20 15:34:45.778 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.779 12468 12487 D color-picker: Brightness set successfully 1 times
04-20 15:34:45.779 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.779 12468 12487 D color-picker: Brightness set successfully 2 times
<...>
04-20 15:34:45.834 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.835 12468 12487 D color-picker: Brightness set successfully 64 times
04-20 15:34:45.835 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.837 12468 12487 D color-picker: Brightness set successfully 65 times
04-20 15:34:45.837 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.837  3176  5876 V WindowManager: Relayout Window{37a74d5 u0 zozzozzz.color_picker/android.app.NativeActivity}: viewVisibility=0 req=720x1232 WM.LayoutParams{(0,0)(fillxfill) sim=#110 ty=1 fl=#81810100 pfl=0x20000 wanim=0x10302fc sbrt=0.9980162 vsysui=0x600 needsMenuKey=2 colorMode=0 naviIconColor=0}
04-20 15:34:45.838 12468 12487 D color-picker: Exception detected
04-20 15:34:45.838 12468 12487 W System.err: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
04-20 15:34:45.838 12468 12487 W System.err:    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8483)
04-20 15:34:45.839 12468 12487 W System.err:    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1428)
04-20 15:34:45.839 12468 12487 W System.err:    at android.view.View.requestLayout(View.java:23221)
04-20 15:34:45.839 12468 12487 W System.err:    at android.view.View.setLayoutParams(View.java:16318)
04-20 15:34:45.840 12468 12487 W System.err:    at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:402)
04-20 15:34:45.840 12468 12487 W System.err:    at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:106)
04-20 15:34:45.840 12468 12487 W System.err:    at android.app.Activity.onWindowAttributesChanged(Activity.java:3201)
04-20 15:34:45.840 12468 12487 W System.err:    at android.view.Window.dispatchWindowAttributesChanged(Window.java:1138)
04-20 15:34:45.841 12468 12487 W System.err:    at com.android.internal.policy.PhoneWindow.dispatchWindowAttributesChanged(PhoneWindow.java:3207)
04-20 15:34:45.841 12468 12487 W System.err:    at android.view.Window.setAttributes(Window.java:1191)
04-20 15:34:45.841  2680  2680 I SurfaceFlinger: id=12125 createSurf (720x1280),1 flag=404, zozzozzz.color_picker/android.app.NativeActivity#0
04-20 15:34:45.841 12468 12487 W System.err:    at com.android.internal.policy.PhoneWindow.setAttributes(PhoneWindow.java:4197)
04-20 15:34:45.841 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.842 12468 12487 D color-picker: Exception detected
04-20 15:34:45.842 12468 12487 W System.err: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

【问题讨论】:

  • 在我看来,您的所有呼叫都发生在同一个线程 (12487) 上。 AFAIK ViewRootImpl 忽略对 requestLayout 的调用,如果它已经在处理布局请求,那么这可能是您的前 65 个 setBrightness 调用发生的情况。 android_main 不在 UI 线程上运行,因此您必须找到一些解决方法。参见例如stackoverflow.com/questions/44808206/…

标签: android c++ multithreading java-native-interface jnienv


【解决方案1】:

感谢comment by Michael,我了解到android_main 确实不在主线程中执行,并且预计我设置亮度的调用通常不应该从那里工作。感谢this answer,我能够让 JNIEnv 调用在主 (UI) 线程中运行,从而使其工作。

花了最多时间发现的是如何获取主线程looper。我想出的技巧是在全局指针的静态初始化程序中运行ALooper_forThread()。这样,这个函数调用就可以保证在dlopens 库的同一个线程中执行,而这个线程恰好是主线程。

适合我的最终代码如下所示:

#include <cmath>
#include <stdexcept>
#include <unistd.h>
#include <jni.h>

#include <android/log.h>
#include <android_native_app_glue.h>

#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG,"color-picker", __VA_ARGS__))

class JNIEnvGetter
{
    JavaVM* javaVM = nullptr;
    JNIEnv* jniEnv = nullptr;
    bool threadAttached = false;
public:
    JNIEnvGetter(ANativeActivity* activity)
        : javaVM(activity->vm)
    {
        // Get JNIEnv from javaVM using GetEnv to test whether
        // thread is attached or not to the VM. If not, attach it
        // (and note that it will need to be detached at the end
        //  of the function).
        switch (javaVM->GetEnv((void**)&jniEnv, JNI_VERSION_1_6))
        {
        case JNI_OK:
            LOGD("No need to attach thread");
            break;
        case JNI_EDETACHED:
        {
            const auto result = javaVM->AttachCurrentThread(&jniEnv, nullptr);
            if(result == JNI_ERR)
                throw std::runtime_error("Could not attach current thread");
            LOGD("Thread attached");
            threadAttached = true;
            break;
        }
        case JNI_EVERSION:
            throw std::runtime_error("Invalid java version");
        }
    }
    JNIEnv* env() { return jniEnv; }
    ~JNIEnvGetter()
    {
        if(threadAttached)
          javaVM->DetachCurrentThread();
    }
};

void setBrightness(ANativeActivity* activity, const float screenBrightness)
{
    LOGD("setBrightness()");
    JNIEnvGetter jeg(activity);
    const auto env=jeg.env();

    const jclass NativeActivity = env->FindClass("android/app/NativeActivity");
    const jclass Window = env->FindClass("android/view/Window");

    const jmethodID getWindow = env->GetMethodID(NativeActivity, "getWindow",
                                 "()Landroid/view/Window;");
    const jmethodID getAttributes = env->GetMethodID(Window, "getAttributes",
                                     "()Landroid/view/WindowManager$LayoutParams;");
    const jmethodID setAttributes = env->GetMethodID(Window, "setAttributes",
                                     "(Landroid/view/WindowManager$LayoutParams;)V");

    const jobject window = env->CallObjectMethod(activity->clazz, getWindow);
    const jobject attrs = env->CallObjectMethod(window, getAttributes);
    const jclass LayoutParams = env->GetObjectClass(attrs);

    const jfieldID screenBrightnessID = env->GetFieldID(LayoutParams, "screenBrightness", "F");
    env->SetFloatField(attrs, screenBrightnessID, screenBrightness);
    env->CallVoidMethod(window, setAttributes, attrs);
    if(env->ExceptionCheck())
    {
        LOGD("Exception detected");
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
    else
    {
        static int count=0;
        LOGD("Brightness set successfully %d times", ++count);
    }

    env->DeleteLocalRef(attrs);
    env->DeleteLocalRef(window);
}

int setBrightnessPipe[2];
void requestSetBrightness(const float brightness)
{
    write(setBrightnessPipe[1], &brightness, sizeof brightness);
}

int setBrightnessCallback(const int fd, const int events, void*const data)
{
    float brightness;
    // FIXME: not ideally robust check
    if(read(fd, &brightness, sizeof brightness)!=sizeof brightness)
        return 1;
    const auto activity=static_cast<ANativeActivity*>(data);
    setBrightness(activity, brightness);
    return 1; // continue listening for events
}

// a funny way to use static initialization to execute something in main thread
const auto mainThreadLooper=ALooper_forThread();

void android_main(struct android_app* state)
{
    ALooper_acquire(mainThreadLooper);
    pipe(setBrightnessPipe);
    ALooper_addFd(mainThreadLooper, setBrightnessPipe[0], 0, ALOOPER_EVENT_INPUT,
                  setBrightnessCallback, state->activity);

    for(float x=0;;x+=0.001)
    {
        int events;
        struct android_poll_source* source;
        while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0)
        {
            if (source)
                source->process(state, source);

            if (state->destroyRequested != 0)
                return;
        }

        requestSetBrightness((1+std::cos(x))/2);
    }
}

【讨论】:

  • 好发现!一个建议:您可以在 JNI_Onload 函数中而不是静态初始化程序中获取 Looper,但效果是一样的。
  • @Botje 不,本机活动不调用此函数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-24
  • 2013-11-09
相关资源
最近更新 更多