Почему один и тот же собственный поток вызывает методы из разных потоков Java?

Я пытаюсь установить яркость экрана для своей деятельности на лету. Сначала, прежде чем войти в цикл 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.

person Ruslan    schedule 20.04.2020    source источник
comment
Мне кажется, что все ваши звонки происходят в одном потоке (12487). AFAIK ViewRootImpl игнорирует вызовы requestLayout, если он уже обрабатывает запрос макета, так что это может быть то, что происходит с вашими первыми 65 вызовами setBrightness. android_main не работает в потоке пользовательского интерфейса, поэтому вам придется найти способ обойти это. См., например. stackoverflow.com/questions/44808206/   -  person Michael    schedule 20.04.2020


Ответы (1)


Благодаря комментарию Майкла я узнал, что android_main действительно не выполняется в основном потоке, и это ожидал, что вызов, который я делал для установки яркости, обычно не должен работать оттуда. Благодаря этому ответу я смог запустить вызовы JNIEnv в основном (UI) потоке, что заставило его работать .

Больше всего времени ушло на выяснение того, как получить петлитель основной нити. Уловка, которую я придумал, состоит в том, чтобы запустить ALooper_forThread() в статическом инициализаторе глобального указателя. Таким образом, этот вызов функции гарантированно будет выполняться в том же потоке, что и библиотека dlopen, которая оказывается основным потоком.

Окончательный код, который работает для меня, выглядит так:

#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);
    }
}
person Ruslan    schedule 20.04.2020
comment
Хорошо подмечено! Одно предложение: вы можете захватить Looper в функции JNI_Onload вместо статического инициализатора, но эффект будет таким же. - person Botje; 21.04.2020
comment
@Botje Botje нет, эта функция не вызывается для нативной активности. - person Ruslan; 21.04.2020