为什么同一个本机线程似乎调用来自不同 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.

多亏了迈克尔的评论,我了解到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);
}
}

最新更新