之前针对手表开发遇到的一个问题,当app弹出dialog后锁屏,然后屏幕就无法操作了,由于是一个全屏的dialog,直接导致测试同事认为屏幕失效.
-
问题表现:dialog弹出后锁屏,然后亮屏,屏幕无法操作,back键无效.
-
追踪log
发现有如下log频繁打印:
Line 20605: 01-09 03:36:00.820 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=96.223854, y[0]=491.9438, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=160615, downTime=159442, deviceId=1, source=0x5002 }
Line 20648: 01-09 03:36:00.845 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=90.276855, y[0]=496.7355, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=3, eventTime=160638, downTime=159442, deviceId=1, source=0x5002 }
Line 20691: 01-09 03:36:00.868 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=89.533676, y[0]=499.87527, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=160661, downTime=159442, deviceId=1, source=0x5002 }
Line 20734: 01-09 03:36:00.891 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=99.987785, y[0]=503.17758, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=160684, downTime=159442, deviceId=1, source=0x5002 }
Line 20777: 01-09 03:36:00.914 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=113.40473, y[0]=502.47736, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=160707, downTime=159442, deviceId=1, source=0x5002 }
Line 20820: 01-09 03:36:00.936 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=123.53931, y[0]=498.88895, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=3, eventTime=160730, downTime=159442, deviceId=1, source=0x5002 }
Line 20863: 01-09 03:36:00.959 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=126.928474, y[0]=495.8939, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=160754, downTime=159442, deviceId=1, source=0x5002 }
Line 20906: 01-09 03:36:00.983 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=126.46416, y[0]=492.6097, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=160777, downTime=159442, deviceId=1, source=0x5002 }
Line 20949: 01-09 03:36:01.006 3584 3584 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=123.98795, y[0]=490.30612, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=3, eventTime=160800, downTime=159442, deviceId=1, source=0x5002 }
分析log发现是框架主动drop事件导致.
查找打印log源代码:
ViewRootImpl.java
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;
}
据此分析必然是 else if 条件成立才导致.由于是必先的bug,直接在框架中加入log打印每一个条件
01-09 03:36:01.005 3584 3584 W ViewRootImpl[MainActivity]: ====================ggplog dropevent start=============================
01-09 03:36:01.005 3584 3584 W ViewRootImpl[MainActivity]: ggplog mAttachInfo.mHasWindowFocus: true
01-09 03:36:01.005 3584 3584 W ViewRootImpl[MainActivity]: ggplog q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER): true
01-09 03:36:01.005 3584 3584 W ViewRootImpl[MainActivity]: ggplog mStopped: true
01-09 03:36:01.005 3584 3584 W ViewRootImpl[MainActivity]: ggplog this....: android.view.ViewRootImpl$EarlyPostImeInputStage@30f5af0
01-09 03:36:01.006 3584 3584 W ViewRootImpl[MainActivity]: ggplog mIsAmbientMode: false
01-09 03:36:01.006 3584 3584 W ViewRootImpl[MainActivity]: ggplog q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON): false
01-09 03:36:01.006 3584 3584 W ViewRootImpl[MainActivity]: ggplog mPausedForTransition: false
01-09 03:36:01.006 3584 3584 W ViewRootImpl[MainActivity]: ggplog isBack(q.mEvent): false
01-09 03:36:01.006 3584 3584 W ViewRootImpl[MainActivity]: ====================ggplog dropevent end=============================
很明显这里mStopped == true 然后进入if条件成立,事件被dropping
继续分析为什么mStopped会为true
必然是框架哪里流程出现问题才导致stopped赋值出错.
字面意思很容易理解,当窗口在前台的时候必然是false状态.当窗口退到后台则必然是true.处于停止状态,这里大家可以直接dumpsys window查看验证一下.
查找设置mStopped值代码,发现只有一个函数在设置值
ViewRootImpl.java
void setWindowStopped(boolean stopped) {
if (mStopped != stopped) {
mStopped = stopped;
final ThreadedRenderer renderer = mAttachInfo.mThreadedRenderer;
if (renderer != null) {
if (DEBUG_DRAW) Log.d(mTag, "WindowStopped on " + getTitle() + " set to " + mStopped);
renderer.setStopped(mStopped);
}
if (!mStopped) {
scheduleTraversals();
} else {
if (renderer != null) {
renderer.destroyHardwareResources(mView);
}
}
for (int i = 0; i < mWindowStoppedCallbacks.size(); i++) {
mWindowStoppedCallbacks.get(i).windowStopped(stopped);
}
if (mStopped) {
mSurface.release();
}
}
}
分析到这里接下来就容易多了,直接打印调用栈
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: java.lang.Throwable
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.view.ViewRootImpl.setWindowStopped(ViewRootImpl.java:1141)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.view.WindowManagerGlobal.setStoppedState(WindowManagerGlobal.java:618)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.app.Activity.performRestart(Activity.java:6779)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.app.Activity.performResume(Activity.java:6816)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3406)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3469)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1527)
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: at android.os.Handler.dispatchMessage(Handler.java:102)
查看WindowManagerGlobal代码:
WindowManagerGlobal.java
public void setStoppedState(IBinder token, boolean stopped) {
synchronized (mLock) {
int count = mViews.size();
for (int i = count - 1; i >= 0; i--) {
if (token == null || mParams.get(i).token == token) {
ViewRootImpl root = mRoots.get(i);
// Client might remove the view by "stopped" event.
root.setWindowStopped(stopped);
// Recursively forward stopped state to View's attached
// to this Window rather than the root application token,
// e.g. PopupWindow's.
setStoppedState(root.mAttachInfo.mWindowToken, stopped);
}
}
}
}
在这里添加log,复现一次bug,锁屏解锁,分别打印log,由于我们是activity弹出dialog,很容易理解这里有两个window, count 值 为:2-1
- 锁屏
01-09 03:35:19.882 3584 3584 V ggplog : stopped...: true
01-09 03:35:19.882 3584 3584 V ggplog : count...: 2
第一次
01-09 03:35:19.882 3584 3584 V ggplog : mParams ( 0). token : android.os.BinderProxy@34056ff
01-09 03:35:19.882 3584 3584 V ggplog : token: android.os.BinderProxy@34056ff
01-09 03:35:19.882 3584 3584 V ggplog : mParams.get(i).token == token
01-09 03:35:19.882 3584 3584 V ggplog : root....: android.view.ViewRootImpl@e1b55b6
第二次
01-09 03:35:19.886 3584 3584 V ggplog : mParams ( 1). token : android.os.BinderProxy@34056ff
01-09 03:35:19.886 3584 3584 V ggplog : token: android.os.BinderProxy@34056ff
01-09 03:35:19.887 3584 3584 V ggplog : mParams.get(i).token == token
01-09 03:35:19.887 3584 3584 V ggplog : root....: android.view.ViewRootImpl@f11104a
- 解锁
01-09 03:35:30.144 3584 3584 V ggplog : stopped...: false
//有两个window
01-09 03:35:30.145 3584 3584 V ggplog : count...: 2
//第一次循环
01-09 03:35:30.145 3584 3584 V ggplog : mParams ( 0). token : android.os.BinderProxy@34056ff
01-09 03:35:30.145 3584 3584 V ggplog : token: android.os.BinderProxy@34056ff
01-09 03:35:30.145 3584 3584 V ggplog : mParams.get(i).token == token
01-09 03:35:30.145 3584 3584 V ggplog : root....: android.view.ViewRootImpl@e1b55b6
01-09 03:35:30.149 3584 3584 W ViewRootImpl[MainActivity]: ggplog this....: android.view.ViewRootImpl@e1b55b6
//第二次循环
//这里mParams ( 1). token : null
01-09 03:35:30.151 3584 3584 V ggplog : mParams ( 1). token : null
01-09 03:35:30.151 3584 3584 V ggplog : token: android.os.BinderProxy@34056ff
从log发现问题,在设置window stop的时候设置了两次,但是解锁以后应该将window设置stop = false却只有一次,第二次token == null,流程无法继续.
-
到这里我们已经有了初步结论,由于token == null,导致setStoppedState流程出错,进而导致window stop状态异常,导致ViewRootImpl判断window是stop状态,然后直接drop事件.
-
接下来我们应该调查为什么token为空?
关于window token大家可以参考这篇博客,讲的还是很清晰明确的.
https://blog.csdn.net/hohohong/article/details/54616053
我们来直接看看Dialog初始化的流程:
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
重点在于 Window w = new PhoneWindow()
phoneWindow继承了window,window 创建的时候同时new 了成员变量mWindowAttributes
private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams()
创建dialog的时候创建了phoneWindow,phoneWindow中new了内部成员变量mWindowAttributes,而attibutes中就包含了这个token,此时new出来之后token还是空值.(可以打log验证)
dialog show的过程中有token赋值过程,跟踪源代码发现token赋值过程
Dailog.show() -> windowmanagerImpl.addview()->windowManagerGlobal.addview()
关于token初始复制过程不在详细分析,如果有兴趣可以看一下addWindow全过程.
可以确认的是token初始值是有的,但是在一次锁屏解锁过程之后就变为空了,那么问题必然就出在update过程中.直接查找ViewRootImpl.updateViewLayout()
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
这里先是拿到params 然后调用了view.setLayoutParams(),最后将wparams add到list列表中。注意我们回到之前看到在setStop的时候就是取了mParams中的params.token.
直接在updateViewLayout()中添加log并且打印trace查看调用关系。
01-17 03:35:46.064 3512 3512 W WindowManager: java.lang.Throwable
01-17 03:35:46.064 3512 3512 W WindowManager: at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:368)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:101)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.app.Dialog.onWindowAttributesChanged(Dialog.java:747)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.view.Window.dispatchWindowAttributesChanged(Window.java:1111)
01-17 03:35:46.064 3512 3512 W WindowManager: at com.android.internal.policy.PhoneWindow.dispatchWindowAttributesChanged(PhoneWindow.java:2954)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.view.Window.setFlags(Window.java:1088)
01-17 03:35:46.064 3512 3512 W WindowManager: at com.android.internal.policy.PhoneWindow$3.onSwipeCancelled(PhoneWindow.java:3039)
01-17 03:35:46.064 3512 3512 W WindowManager: at com.android.internal.widget.SwipeDismissLayout.cancel(SwipeDismissLayout.java:282)
01-17 03:35:46.064 3512 3512 W WindowManager: at com.android.internal.widget.SwipeDismissLayout$2$1.run(SwipeDismissLayout.java:98)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.os.Handler.handleCallback(Handler.java:751)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.os.Handler.dispatchMessage(Handler.java:95)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.os.Looper.loop(Looper.java:154)
01-17 03:35:46.064 3512 3512 W WindowManager: at android.app.ActivityThread.main(ActivityThread.java:6167)
01-17 03:35:46.064 3512 3512 W WindowManager: at java.lang.reflect.Method.invoke(Native Method)
01-17 03:35:46.064 3512 3512 W WindowManager: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
01-17 03:35:46.064 3512 3512 W WindowManager: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
这里我们发现了一个比较有意思的东西SwipeDismissLayout,直接百度
SwipeDismissFrameLayout是一个右滑退出的布局。联想到我们手表所有view都可以右滑退出,那么我们可以想象一下一定是右滑的时候更新了dialog的参数。锁屏的时候也可能对当前top window做了右滑的操作,不过这已经不重要了。我们现在重要的是看看SwipeDismissFrameLayout到底怎么更新dialog对应window参数的
swipeDismiss.setOnSwipeProgressChangedListener(
new SwipeDismissLayout.OnSwipeProgressChangedListener() {
@Override
public void onSwipeProgressChanged(
SwipeDismissLayout layout, float alpha, float translate) {
WindowManager.LayoutParams newParams = getAttributes();
newParams.x = (int) translate;
newParams.alpha = alpha;
setAttributes(newParams);
int flags = 0;
if (newParams.x == 0) {
flags = FLAG_FULLSCREEN;
} else {
flags = FLAG_LAYOUT_NO_LIMITS;
}
setFlags(flags, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
}
@Override
public void onSwipeCancelled(SwipeDismissLayout layout) {
WindowManager.LayoutParams newParams = getAttributes();
// Swipe changes only affect the x-translation and alpha, check to see if
// those values have changed first before resetting them.
if (newParams.x != 0 || newParams.alpha != 1) {
newParams.x = 0;
newParams.alpha = 1;
setAttributes(newParams);
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
}
}
});
}
看到这里先是getAttibutes()修改一些参数之后又调用了setAttributes()把参数set回去。
我们打印log查看getAttibutes中的token,看第一次调用的时候token值是什么?
通过我们打印log get 和set 对应的token都是空的,那么也就是从一开始token就是空的,不怪SwipeDismissFrameLayout,这里并没有什么错误。
所以setAttributes()之后自然就是空了。然后又update了params参数。
那么问题又来了,最初dialog的token还赋值了这会怎么就空了?
既然一开始token是有值的,但是后来get的时候是空的,那么有两个方向可以怀疑:
1.中间某个流程设置为null
2.初始化有错误,get了一个错误地址.
回到dialog.show()看看源代码:
public void show() {
.....
WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
}
我们分析一下源代码:
WindowManager.LayoutParams l = mWindow.getAttributes();
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
....
l = nl
mWindowManager.addView(mDecor, l)
我们知道java除了基本类型是值引用,其他都是地址引用.
这里后期不管怎么给 l 赋值,最后都给到了nl
这是一个很傻逼的写法,我们知道后期需要get window参数的时候肯定是从mWindow.getAttributes()中拿到.
这就导致L中所有参数几乎都没有值.
也就是说一开始token是没有值的,在addwindow的时候将 token复制给了nl,但是这个token并没有给到l,等待再次需要的时候又从l中获取参数.当然就是空了.
*总结
到此,我们已经明白了token为空的原因,那么怎么修复这个bug呢?
由于我们源代码是Android7.0 ,所以我到Android9.0中查看了一下dialog源代码.


看到google在9.0中已经修复了这个错误代码.
网友评论