Android P (API 28) 弹窗按键无响应 Dropping event due to no window focus
发现在Android 28§版本上出现弹窗之后,按键无反应,只能重启,而其他版本的手机却没有问题
通过抓日志,看到在按键的时候,日志里不断出现下面两条日志:
W/ViewRootImpl: Dropping event due to no window focus: KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_DPAD_DOWN, scanCode=108, metaState=0, flags=0x8, repeatCount=0, eventTime=95320779, downTime=95320779, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
跟踪源码发现是在 ViewRootImpl 中抛出的异常,可以看到有四种情况可以进入该异常
protected boolean shouldDropInputEvent(QueuedInputEvent q) {
if (mView == null || !mAdded) {
Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
return true;
} else if ((!mAttachInfo.mHasWindowFocus
&& !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
&& !isAutofillUiShowing()) || mStopped
|| (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
|| (mPausedForTransition && !isBack(q.mEvent))) {
// This is a focus event and the window doesn't currently have input focus or
// has stopped. This could be an event that came back from the previous stage
// but the window has lost focus or stopped in the meantime.
if (isTerminalInputEvent(q.mEvent)) {
// Don't drop terminal input events, however mark them as canceled.
q.mEvent.cancel();
Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent);
return false;
}
// Drop non-terminal input events.
Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);
return true;
}
return false;
}
断点调试不好使,最终通过打印日志定位问题
mAttachInfo.mHasWindowFocus = true
mIsAmbientMode = false
mStopped = false
mPausedForTransition = false
通过判断最终确定是由于 mAttachInfo.mHasWindowFocus = false,导致一直无法处理按键事件,抛出按键异常。
为什么 mAttachInfo.mHasWindowFocus = false呢?明明应该是当前Dialog获得了焦点,却是false呢?
下面对mAttachInfo.mHasWindowFocus赋值的地方打印日志进行判断
正常情况下的流程
- windowFocusChanged
- performTraversals
mAttachInfo.mHasWindowFocus = false- handleWindowFocusChanged
mAttachInfo.mHasWindowFocus = true
最终mAttachInfo.mHasWindowFocus = true
异常情况下的流程
- windowFocusChanged
- deliverInputEvent
mAttachInfo.mHasWindowFocus = false- handleWindowFocusChanged
mAttachInfo.mHasWindowFocus = true- performTraversals
mAttachInfo.mHasWindowFocus = false
最终mAttachInfo.mHasWindowFocus = true
通过流程对比可以发现 ,主要是handleWindowFocusChanged和performTraversals的流程调换了导致mAttachInfo.mHasWindowFocus = false
下面是上面涉及的所有主要方法
// 异步Binder线程执行
public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
synchronized (this) {
mWindowFocusChanged = true;
mUpcomingWindowFocus = hasFocus;
mUpcomingInTouchMode = inTouchMode;
}
Message msg = Message.obtain();
msg.what = MSG_WINDOW_FOCUS_CHANGED;
mHandler.sendMessage(msg);
}
// Handler里面执行
private void handleWindowFocusChanged() {
...
mAttachInfo.mHasWindowFocus = hasWindowFocus;
...
}
// Handler里面执行按键事件
private void deliverInputEvent(QueuedInputEvent q) {
...
if (stage != null) {
// Android 28 增加了这一行,28以下没有这一行
handleWindowFocusChanged();
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
Dialog.show()-->WindowManagerImpl.addView(mDecor, l)-->WindowManagerGlobal.addView()-->ViewRootImpl.setView()-->requestLayout()-->scheduleTraversals()
// 对Handler加锁,只能执行async的方法,普通Handler暂不执行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 继续执行Input的Handler消息
scheduleConsumeBatchedInput()-->doProcessInputEvents()-->deliverInputEvent(q)
// 对Handler加锁,以后可以执行普通Handler
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 解锁之后,马上执行performTraversals,然后执行其他普通Handler
private void performTraversals() {
···
if (mFirst) {
...
// Android 29 删除了这一行
mAttachInfo.mHasWindowFocus = false;
...
}
···
mFirst = false;
...
}
Dialog.show() 方法最终会对Handler加锁,除了deliverInputEvent方法,只能先运行performTraversals方法,所以如果没有按键事件deliverInputEvent,handleWindowFocusChanged正常会在performTraversals之后
有按键事件deliverInputEvent,handleWindowFocusChanged就会在performTraversals之前,造成mAttachInfo.mHasWindowFocus 始终为 false,出现按键异常,无法获焦
那为什么Android 29和Android 27及以下的版本没问题呢?
1、Android 29没问题
在performTraversals删了mAttachInfo.mHasWindowFocus = false代码,所以自然mAttachInfo.mHasWindowFocus != false
只会在Android 28 版本出现
private void performTraversals() {
···
if (mFirst) {
...
// Android 29 删除了这一行
mAttachInfo.mHasWindowFocus = false;
...
}
···
mFirst = false;
...
}
2、Android 27及以下没问题
Android 28版本在deliverInputEvent增加了一行代码,也就是增加处理handleWindowFocusChanged逻辑。
所以Android 27及以下,在deliverInputEvent里面根本就没有handleWindowFocusChanged处理,handleWindowFocusChanged就放在了performTraversals之后
// deliverInputEvent按键事件里面,Android 28 增加了这一行,27及以下没有这一行
private void deliverInputEvent(QueuedInputEvent q) {
...
if (stage != null) {
// Android 28 增加了这一行,27及以下没有这一行
handleWindowFocusChanged();
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
解决办法
在Dialog、PopupWindow、DialogFragment都会有该问题
参考解决办法,在dispatchKeyEvent方法里面,通过反射把mAttachInfo.mHasWindowFocus重置为true
我是在TV上遇到的,都是用按键操作的,所有可以用dispatchKeyEvent。不过手机上源码也是一样的,可以用类似的解决办法,在合适的时机如dispatchTouchEvent,通过反射把mAttachInfo.mHasWindowFocus重置为true
@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
if (Build.VERSION.SDK_INT == 28) {
LogUtils.d(TAG, event.toString());
try {
ViewParent viewRootImpl = getWindow().getDecorView().getParent();
Class viewRootImplClass = viewRootImpl.getClass();
Field mAttachInfoField = viewRootImplClass.getDeclaredField("mAttachInfo");
mAttachInfoField.setAccessible(true);
Object mAttachInfo = mAttachInfoField.get(viewRootImpl);
Class mAttachInfoClass = mAttachInfo.getClass();
Field mHasWindowFocusField = mAttachInfoClass.getDeclaredField("mHasWindowFocus");
mHasWindowFocusField.setAccessible(true);
mHasWindowFocusField.set(mAttachInfo, true);
boolean mHasWindowFocus = (boolean) mHasWindowFocusField.get(mAttachInfo);
LogUtils.i(TAG, "dispatchKeyEvent set mHasWindowFocus to true and get it as: ", mHasWindowFocus);
} catch (Exception e) {
LogUtils.i(TAG, "dispatchKeyEvent set mHasWindowFocus to true error: ", e.toString());
e.printStackTrace();
}
}
return super.dispatchKeyEvent(event);
}