背景
最近App开发同事发现了个系统Bug, Dialog显示后, 电源键灭屏后再亮屏, 此时Dialog无法点击,是个基本上必现的bug, Android系统版本为Android 7.1.1 .
问题分析
首先发现这个bug后, 可以确定的是这个bug一定是我们自己修改系统相关功能或者代码引进的, 而不是Android系统本身的问题, 毕竟很容易复现并且暴露给用户. 首先就来看下点击失效时的Log, 然后就发现了如下Log:
W ViewRootImpl[MainActivity]: Dropping event due to no window focus
可以看到是由于失去焦点而忽略了点击事件, 因此下面就得从Log打印位置出手跟踪原始流程来定位为什么失去焦点了.
问题定位
在OpenGrok搜索Log中关键字, 找到对应函数, 代码如下:
frameworks/base/core/java/android/view/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)) || 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判断条件里面去了, 因此我们需要将4个判断条件值打印出来, 看看是那个条件导致流程走到了这里, 加入如下Log来打印相关判断条件:
android.util.Log.e("wenzhe1", "1:" + (!mAttachInfo.mHasWindowFocus && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)));
android.util.Log.e("wenzhe1", "2:" + mStopped);
android.util.Log.e("wenzhe1", "3:" + (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)));
android.util.Log.e("wenzhe1", "4:" + (mPausedForTransition && !isBack(q.mEvent)));
加入Log后, 编译刷机, 重新复现一下bug, 点击Dialog后, Log打印如下:
06-15 13:14:10.795 3621 3621 E wenzhe1 : 1:false
06-15 13:14:10.795 3621 3621 E wenzhe1 : 2:true
06-15 13:14:10.795 3621 3621 E wenzhe1 : 3:false
06-15 13:14:10.795 3621 3621 E wenzhe1 : 4:false
可以看到是第二个条件即mStop
为true导致流程异常了, 看下 mStop
定义位置的注释,:
// Set to true if the owner of this window is in the stopped state,
// so the window should no longer be active.
boolean mStopped = false;
可以看到, 当Window处于Stop状态, ViewRootImpl也要置为Stop状态, 那么看到这里基本对问题出现原因有了大致了解: Dialog的Window状态出现了异常, 亮屏后并没有将 mStop
置为false.
接着定位具体问题点, 在当前文件中搜索一下, 可以发现改变mStop
值只有一个地方, 就是 void setWindowStopped(boolean stopped)
函数, 我们在此函数中加个Log打印一下调用堆栈, 看看是那个地方会调用:
void setWindowStopped(boolean stopped) {
if (mStopped != stopped) {
//打印调用堆栈
android.util.Log.e("wenzhe2", android.util.Log.getStackTraceString(new Throwable()));
mStopped = stopped;
final ThreadedRenderer renderer = mAttachInfo.mHardwareRenderer;
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);
}
}
}
}
重新复现bug跑下流程, 定位调用的地方为:
frameworks/base/core/java/android/view/WindowManagerGlobal.java
中的 setStoppedState(IBinder token, boolean stopped)
, 这次我们需要将 mParams
链表中的值和token
的值打印出来, 看看灭屏前后里面值的区别, 来进一步定位问题, 在函数中加入如下Log:
public void setStoppedState(IBinder token, boolean stopped) {
synchronized (mLock) {
int count = mViews.size();
// 加入调试Log
android.util.Log.e("wenzhe3", "view count:" + count + " token:" + token);
for (WindowManager.LayoutParams par : mParams) {
android.util.Log.e("wenzhe3", "params token:" + par.token);
}
for (int i = 0; i < count; i++) {
if (token == null || mParams.get(i).token == token) {
ViewRootImpl root = mRoots.get(i);
root.setWindowStopped(stopped);
}
}
}
}
加入Log后, 又是编译刷机复现bug跑下流程, 打印Log如下:
// 灭屏Log, 此时操作是将两个ViewRoot置为stop状态
06-15 13:02:09.503 3621 3621 E wenzhe3 : view count:2 token:android.os.BinderProxy@5b7e3c
06-15 13:02:09.504 3621 3621 E wenzhe3 : params token:android.os.BinderProxy@5b7e3c
06-15 13:02:09.504 3621 3621 E wenzhe3 : params token:android.os.BinderProxy@5b7e3c
// 亮屏Log 此时操作是将两个ViewRoot置为非stop状态
06-15 13:02:12.859 3621 3621 E wenzhe3 : view count:2 token:android.os.BinderProxy@5b7e3c
06-15 13:02:12.859 3621 3621 E wenzhe3 : params token:android.os.BinderProxy@5b7e3c
06-15 13:02:12.859 3621 3621 E wenzhe3 : params token:null
看到这里, 很容易发现, 灭屏后再亮屏, mParams
链表中有一个token变为null, 导致亮屏时, 两个ViewRoot(一个Activity的, 一个Dialog的)只有第一个执行了 setWindowStopped()
, 所以 Dialog的ViewRoot的mStop
在亮屏时没有被置为 false, 所以后续的点击事件被忽略了. 问题原因找到了, 现在就要定位是谁导致了这个原因.
同样在当前文件中搜索 mParams
, 查找添加和删除元素的地方, 打印Log定位, 步骤和上面类似, 就不贴代码了, 最后定位是在 void updateViewLayout(View view, ViewGroup.LayoutParams params)
中, 添加的LayoutParams的token是null, 所以导致后面的问题, 同样打印调用堆栈, 继续定位具体是哪里调用的, 添加Log如下:
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);
// token 为 null就打印调用堆栈, 定位具体调用位置
if (wparams.token == null)
android.util.Log.e("wenzhe2", android.util.Log.getStackTraceString(new Throwable()));
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
打印结果如下:
E wenzhe2 : java.lang.Throwable
E wenzhe2 : at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:383)
E wenzhe2 : at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:101)
E wenzhe2 : at android.app.Dialog.onWindowAttributesChanged(Dialog.java:723)
E wenzhe2 : at android.view.Window.dispatchWindowAttributesChanged(Window.java:1098)
E wenzhe2 : at com.android.internal.policy.PhoneWindow.dispatchWindowAttributesChanged(PhoneWindow.java:2940)
E wenzhe2 : at android.view.Window.setAttributes(Window.java:1129)
E wenzhe2 : at com.android.internal.policy.PhoneWindow.setAttributes(PhoneWindow.java:3804)
E wenzhe2 : at com.android.internal.policy.PhoneWindow$3.onSwipeCancelled(PhoneWindow.java:3039)
E wenzhe2 : at com.android.internal.widget.SwipeDismissLayout.cancel(SwipeDismissLayout.java:307)
E wenzhe2 : at com.android.internal.widget.SwipeDismissLayout$2$1.run(SwipeDismissLayout.java:101)
E wenzhe2 : at android.os.Handler.handleCallback(Handler.java:751)
E wenzhe2 : at android.os.Handler.dispatchMessage(Handler.java:95)
E wenzhe2 : at android.os.Looper.loop(Looper.java:154)
E wenzhe2 : at android.app.ActivityThread.main(ActivityThread.java:6120)
E wenzhe2 : at java.lang.reflect.Method.invoke(Native Method)
E wenzhe2 : at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
E wenzhe2 : at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
看到这个Log, 就确定了问题所在, 引起这个问题的就是我们系统中加入的SwipeDismissLayout(右滑退出)功能引起的, 这个功能之前我有写过一片文章:点击传送, 看下SwipeDismissLayout源码就知道, 在灭屏的时候会根据当前滑动状态, 来更新一下window的位置(是cancel还是dismiss), 而更新的时候, 要获取Window的属性 WindowManager.LayoutParams newParams = getAttributes();
而此时获取的属性中, newParams.token
值为 null, 所以导致后面一系列问题.
此部分代码位置: frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
函数为: private void registerSwipeCallbacks()
, 有兴趣可以看下.
解决问题
问题点定位后, bug就好解决了, 先验证下是不是SwipeDismissLayout引起的, 由于我们系统默认是启用右滑退出功能的, 默认Dialog也能右滑退出, 所以需要给测试的Dialog加个主题,
主题中加入:<item name="android:windowSwipeToDismiss">false</item>
然后运行测试, bug不复现, 证实就是SwipeDismissLayout引起.所以解决问题基本就两种方式:
- 给Dialog默认都加上
<item name="android:windowSwipeToDismiss">false</item>
样式 - 在Dialog源码中,将PhoneWindow的FEATURE_SWIPE_TO_DISMISS这个feature去掉即可, 这个需要通过添加函数和修改一点PhoneWindow.java中的逻辑来实现.
一些细节
上面只是大概说明了引起问题的原因, 有些细节还没说明白:为什么Dialog内部的PhoneWindow中, 通过getAttributes()得到的LayoutParams.token为null, 而Activity是正常的, 并且在灭屏之前, mParams
链表里面的token为什么是正常的?
LayoutParams.token
是在Window.java中 void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp)
进行赋值的, 而调用此函数是在WindowManagerGlobal.java的addView()
中,代码如下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//部分代码省略...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
//调用此函数后, wparams.token就不为null了
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
//部分代码省略...
}
所以只要parentWindow
不为空, 则会进行调整, 调整后Token就不为空了, 所以addView时候添加的LayoutParams的Token不为空, 所以灭屏前mParams
里面Token也不为空.
- addView()阶段LayoutParams.token会被重新赋值, 即token已经被存储到LayoutParams中了, 但灭屏时调用的updateViewLayout()传递的LayoutParams.token为空, 只能说明这两个不是同一对象, 从前面分析的流程可知, updateViewLayout()函数中的参数LayoutParams是通过调用Window的getAttributes()来得到的, 是当前Window对象的LayoutParams. 而addView()中的的参数LayoutParams则是如下代码获得的:
frameworks/base/core/java/android/app/Dialog.java
public void show() {
//部分代码省略...
WindowManager.LayoutParams l = mWindow.getAttributes();
// 如果满足条件, 会重新new一个对象, 所以后后续调用adjustLayoutParamsForSubWindow()
//生成的Token并没有存储到当前mWindow对象中,后续getAttributes()中的token就为空
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);
//部分代码省略...
}
上面代码中, 满足if判断条件后, 重新创建了一个对象, 所以后续token并没有存储到本身的window中, 通过打印Log, 证实默认情况下if判断条件为true, 所以updateViewLayout()时LayoutParams并不是当前Window对象的, 所以后续getAttributes()获得的LayoutParams.token依然为null.
我们将if条件内容先直接注释调, 看看此种方式是否能解决我们遇到的Bug, 实测证明Bug完美解决,
可以看到, 在找到更深层次原因后, 又多了一种解决bug的方法.
总结
- Bug产生的原因是Dialog所在的Window灭屏后, ViewRoot没有正常执行setWindowStopped()函数导致的.
- 造成setWindowStopped()流程异常原因是使用了SwipeDismissLayout后, 灭屏后会更新当前Window的LayoutParams, 此时通过getAttributes()获取的LayoutParams中的token为null, 所以造成后面流程出错, 禁用SwipeDismissLayout即可解决bug.
- Dialog由于其特殊性, 在调用show()函数时, 会重新创建WindowManager.LayoutParams对象, 导致后面调用addView()的时候, 生成的Token没有被存储到当前的Window对象的LayoutParams中, 而是存储到了new出来的LayoutParams对象中, 所以后续调用getAttributes()获取的LayoutParams中的token为null.
总的来说, 分析过程要有耐心, 对问题分析的越深入, 会找到更多解决Bug的方法.
网友评论