android调用dialog.hide()引起的输入事件派发错误问题追踪

时间:2022-04-26
本文章向大家介绍android调用dialog.hide()引起的输入事件派发错误问题追踪,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

问题描述:某个界面启动后,上面的actionbar的item点击不起作用 问题调研:

00

在activity的启动过程中,创建了一个Fragment.java,在Fragment.java的createView回调中,调用了一个线程,线程中使用postUI调用dialog.show(),然后加载图片,如果没有图片,会postUi调用dialog.hide()隐藏,之后activity上面的actionbar Item点击没响应。 初步怀疑,是由于Fragment.java的写法有误,导致没有调用onCreateOptionsMenu,引起onOptionsItemSelected没有响应。但是通过断点跟踪,发现不是,这里的onCreateOptionsMenu调用了。按照网上的说法是加入setHasOptionsMenu( true );,查看代码是有此逻辑,因此可以确定,这块添加的代码是没有问题的。 于是上断点,调试DecorView.java的dispatchTouchEvent方法,为什么调试的是DecorView.java呢?因为我们activity在使用setContentView将一个布局加载起来时候,实际挂在DecorView的目录树里,因此这里便是事件的分派地方,当然,如果要说activity和inputmanager的消息传递位置,会在ViewRootImpl.java的onInputEvent方法里面。

我们在DecorView.java的dispatchTouchEvent方法打上断点,然后点击actionbar的item,然后发现这里的信息

发现这里的cb是个ProcessDialog,于是得出结论,这个当前屏幕上虽然看不到对话框(使用hide()隐藏掉),但是inputmanager那边,却还是将此事件传递给了它,所以初步结论,focus window出现错误,导致事件派发错误,引出问题。

那么,我们继续深究,从inputmanager这里,先进行一个初步判断 电脑连上手机,使用 adb shell dumpsys >~/1.txt 将dump信息存储下来,然后打开1.txt 搜索 Input Dispatcher State:

这里可以找到input可以传递的一个窗口列表 这里关键的几个信息: FocusedApplication :当前焦点app FocusedWindow: name='Window{f8c1e72 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}' 当前focus的窗口信息 后面紧跟着一堆窗口列表:

列表的一些信息: name='Window{1781b28 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}' 窗口名字,以及内存地址,title displayId=0 显示在哪个屏幕id上,默认为0,可以是其他,比如我们投屏到电视,或者模拟虚拟的屏幕上。 hasFocus=false 是否获取焦点 visible=true 是否可见 canReceiveKeys=false 是否处理按键消息 layer=21025 当前在绘制里面的层大小,这个值越大,代表z序列越高,屏幕显示是按照z排序进行绘制,从低向高,如果高的layer是个全屏,则会将低值的那些界面全部覆盖。 frame=[27,780][1053,1068] 此窗口在屏幕上的布局大小 touchableRegion=[0,0][1080,1920] 此窗口的可点击区域 然后我们查找代码,去看下输入服务那边,是如何判断发送给谁的呢?

我们找到InputDispatcher.cpp的findTouchedWindowAtLocked,可以看到,这里关键的信息是:windowInfo->visible,由于我们排列顺序是从前往后,因此第一个遍历到对话框窗口的时候,发现 windowInfo->visible=True,因此系统会将触摸消息,发送给这个窗口,也就是对话框。然而,实际上对话框在apk这边,已经是隐藏状态,同时自身也不消耗触摸事件,因此导致事件一直发给一个隐藏的窗口,引出问题。

01

到这里,就完了?那你还是比较年轻。虽然最终的解决方案是使用dismiss替换掉了hide,但是我们不能停留在这个表象,继续深挖下此问题。问题最终的解决,只是规避了出现此问题,但是最根本的原因,我们还需要继续寻找。

我们知道了这里有个mWindowHandles列表存储了当前的窗口,并且已经排序,那么我们找下,这个值是谁给的,因此我们在本文件查找,发现了关键方法setInputWindows,

这里会将窗口赋值进来。然后我们全局搜索setInputWindows,最终在 InputMonitor.java的 updateInputWindowsLw方法里面,锁定了关键逻辑。

在updateInputWindowsLw里面,我们发现了一段很关键的代码

这里有个方法isVisible = child.isVisibleLw();会去更新显示状态,我们之前看到,就是这个变量是Ture,导致系统认为我们的对话框是可见,引出的问题。 于是我们的重心,转移到了这里,我们看下代码:

我们主要关心!mAnimatingExit && !mDestroying 这两个值(其他本身也是要关注,但是因为已经跟过,知道他们不变,所以去掉了那些无关的变量)

02

当前窗口的信息,这些变量如何得知的呢?我们来看个推演过程,我们之前使用adb shell dumpsys的文档,打开, 我们通过 Input Dispatcher State,找到了当前focus的是romListActivity,但是显示的有两个,一个是activity的主窗口,一个是对话框的窗口,对话框的layer比activity的layer高,因此它优先得到了触摸响应。 具体对话框的信息如下:

我们使用这里的 name='Window{1781b28 的1781b28,在文本中搜索,可以找到window的详细信息:

mHasSurface=true mPolicyVisibility =true mAttachedHidden=false mAnimatingExit=false mDestroying=false mIsWallpaper=false mWallpaperVisible=xxx 关于这些值怎么算出来的,是通过这里的dump信息,我们找到windowState.java的dump,我们调用的dumpsys命令,会走到这里,

然后这里的dump方法有这段逻辑,通过查看,我们的dumpsys里面没有出现这些数据,因此它们的值就可以确定出来的。

03

当前情况,我们是没法知晓到底是哪个值引起的问题,然后如果我们直接去看代码,分析定位到底是哪个值引起,那你会崩溃掉的,系统里面,最不喜欢跟踪的就是显示隐藏,以及动画过程,太过杂乱,很多方法频繁调用,输出的log信息过多,逻辑错综复杂,很难把握,跟进这种问题,往往太耗精力。 我这里尝试使用demo来测试,写了如下代码:

也就是把出问题的那段逻辑,搬出来独立测试下,发现没有问题,这样子我们就可以进行对比了。然后通过dumpsys之后,发现了关键数据,在dump里面,出现了一些数据:

我们发现,这里的mDestroying=true,所以这时的dialog.hide ()之后,窗口就不会获取焦点,同时也不是显示状态,逻辑正常。 通过对比,我们发现线索,可以追踪mDestroying是何时进行更新,变成true的。 我们找了很多地方,同时在每个地方,进行添加log信息,然后抓取log。同时将Windowmanage的调试信息全部打开(将WindowManagerDebugConfig.java里面的所有变量为false全部置成true),然后编译mmm frameworks/base/services ,make snod打包,然后将system.img刷入手机,再次进行复现问题,同时抓取log,通过查阅log,可以得出结论, 系统在修改mDestroying的地方,最终锁定在WindowStateAnimator.java的finishExit方法中。

这条线追到这里,那么我们就在代码查找这个finishExit里面的 这段 finishExit in 信息,想从log信息中,找到一些蛛丝马迹。

搜索得到一些数据,我们可以使用后面 的WindowStateAnimator{91b6679 这里的91b6679便是地址,那么我们从dumpsys里面,找到当前dialog窗口的动画地址,91b6679

所以我们就可以锁定到我们 dialog窗口的动画是哪个log了。

我们继续查找,使用91b6679,发现了一段异常逻辑。

这里前面可以看到,对应的窗口已经在退出window{1781b28 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity EXITING}

log中的addInputWindowHandle就是系统设置input信息的地方,可以确定这里这个对话框窗口已经在退出中

也就是mAnimatingExit=true,根据之前的isVisibleUnchecked逻辑可知,这里如果mAnimatingExit=true,那么InputMonitor.java里面的updateInputWindowsLw得到的 final boolean isVisible = child.isVisibleLw();就是false了,也就是ok的了。 通过紧跟着的log继续去看,发现了出错地方: Update reported visibility: Win Window{f8c1e72 这个窗口是activity的,问题点就在这里,这里会更新,让对应的VIS AppWindowToken{2090d 显示出来,而我们的对话框,是在这个VIS AppWindowToken{2090d里面的。因为它是activity的子窗口。

于是,紧跟着的log就出现了如下语句:

OPEN TRANSACTION handleAppTransitionReadyLocked()
performing show on: WindowStateAnimator{91b6679  我们的动画重新更新了,也就不退出来。
performShow on WindowStateAnimator{91b6679 
performing show on: WindowStateAnimator{9e9f896 这里是我们的activity对应的动画。
performShow on WindowStateAnimator{9e9f896

出错就在这里。然后我们需要看下这个逻辑,是怎么出现的,通过定位代码,搜索关键字handleAppTransitionReadyLocked 找到问题点。最终我们找到,代码在 WindowSurfacePlacer.java 的 handleOpeningApps方法里面。

同时我们在handleAppTransitionReadyLocked方法中,看到如下语句:

可以看到,这时我们的标志被清除掉了,引发了问题。

然后我们在handleOpeningApps 里面,找到一段log文字Now opening app,通过检索log,对比正确与错误的log备份,发现了问题。

正确的:

9886 start u0
11790 relayout dialog viewVisibility=0
12828 relayout activity viewVisibility=0
14740 WindowSurfacePlacer: **** GOOD TO GO
14883 Now opening appAppWindowToken
14946 dialog handleOpeningApps
15133 activity handleOpeningApps
15691 realyout dialog viewVisibility=8

出问题的:

3018 start u0
9023 relayout dialog viewVisibility=0
11788 relayout activity viewVisibility=0
14912 relayout dialog viewVisibility=8
19169 WindowSurfacePlacer: **** GOOD TO GO
19337 Now opening app
19403  dialog  activity handleOpeningApps

出问题的时候,这个handleOpeningApps的调用时机,远远晚于了dialog.hide的过程,因此在后续更新activity的时候,意外的将其子窗口的动画进行了重置,引发此问题。

04

这里我们再进行扩展下:我们跟踪下dialog.hide()方法,可以看到这里只是简单的修改了根节点View的显示属性。

那么这个属性在哪里被检测到的呢?我们知道,每个activity对应一个ViewRootImpl,系统实时都会调用这里的

这里performTraversals里面有个方法,叫做 final int viewVisibility = getHostVisibility();会拿到刚才hide()设置的那个View的显示隐藏状态,如果发生改变,会调用这里的

然后这里的relayoutWindow实质的代码位置,在:

mWindowSession.relayout 
-->mService.relayoutWindow(Session.java)
-->relayoutWindow(WindowManagerService.java)

在这个方法里面,也输出来一段关键log,这里为Relayout ...: viewVisibility= 我们可以使用: viewVisibility= 去搜索log,然后使用viewVisibility=8 进行过滤,因为8=View.GONE,从而可以得出,dialog.hide()真正被系统处理的时间。错误的时候,因为触发的时机过早,导致后续的activity还没open起来,子窗口却意外的要去隐藏,导致更新时错误,引发问题。 错误的时候 01-02 16:56:39.790 982 2627 V WindowManager: Relayout Window{1781b28 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}: viewVisibility=8 然后handleOpeningApps的时间 01-02 16:56:39.956 982 1270 I WindowManagerService: at com.android.server.wm.WindowSurfacePlacer.handleOpeningApps(WindowSurfacePlacer.java:1246) 所以是在后面,导致dialog的hide被冲掉了。 正确的时候:(demo应用) 01-02 21:13:21.580 982 11320 I WindowManagerService: at com.android.server.wm.WindowSurfacePlacer.handleOpeningApps(WindowSurfacePlacer.java:1246) 然后才是隐藏: 01-02 21:13:26.939 982 7983 V WindowManager: Relayout Window{123729 u0 wwww}: viewVisibility=8 req=1026x483 WM.LayoutParams{(0,0)(wrapxwrap) gr=#11 sim=#120 ty=2 fl=#1820002 fmt=-3 wanim=0x1030466 surfaceInsets=Rect(96, 96 - 96, 96) needsMenuKey=2} 这个就是正确的了,系统就会判断dialog的状态是销毁中,隐藏状态,未获取焦点,输入触摸事件,则会正确的传递给对应的activity。

此问题还没追踪结束,我们继续来看log,继续细化log,再次看下问题:

正确的:

9886 start u0
11040 WindowManager: handleMessage: entry what=2 就是 REPORT_FOCUS_CHANGE = 2

11790 relayout dialog viewVisibility=0

12828 relayout activity viewVisibility=0

14127  WindowManager: handleMessage: entry what=4  就是  DO_TRAVERSAL = 4这个4是关键
14740 WindowSurfacePlacer: **** GOOD TO GO

14883 Now opening appAppWindowToken

14946 dialog handleOpeningApps

15133 activity handleOpeningApps

15691 realyout dialog viewVisibility=8

出问题的:

3018 start u0

6627 WindowManager: handleMessage: entry what=2 就是 REPORT_FOCUS_CHANGE = 2聚焦到dialog
9023 relayout dialog viewVisibility=0

11788 relayout activity viewVisibility=0

12595 WindowManager: handleMessage: entry what=41

14912 relayout dialog viewVisibility=8

15576 WindowManager: handleMessage: entry what=2 就是 REPORT_FOCUS_CHANGE = 2切换到acitivty
18851 WindowManager: handleMessage: entry what=4  就是  DO_TRAVERSAL = 4这个4是关键 ,同步更新 
19169 WindowSurfacePlacer: **** GOOD TO GO

wtoken.clearAnimatingFlags(); 将标识在这里清掉了,导致设置的隐藏状态消失。

19337 Now opening app
19403  dialog  activity handleOpeningApps

可以看到,同步的消息必须在隐藏前被调用一次,否则便会出错。这里的同步是在WindowSurfacePlacer.java代码里面

于是,我们又需要去检查,出错的时候,为什么requestTraversal方法,触发的时机慢了一些。或者说是hide()的处理时机,为什么超前了一些呢?

错误的:

72057 22:50:44.369 start u0

73349 01-03 22:50:44.646 24013 24050 I Thread xxx: run 0---- 277ms

75853 22:50:44.947 hide dialog 586ms 01-03 22:50:44.947 24013 24013 I Thread xxx: run 1----

76475 relayout dialog 隐藏

77317 22:50:45.078 finishDrawingWindow 709ms

正确的:

84501 22:55:47.726 start u0

87357 22:55:47.893 24439 24439 I Thread xxx: run 0---- 167ms

96824 ViewRootImpl[wwww]: FINISHED DRAWING: wwww

96843 22:55:48.427 finishDrawingWindow: Window{b8c0aef u0 wwww} 701ms

98403 22:55:48.520 hide dialog 794ms 01-03 22:55:48.520 24439 24439 I Thread xxx: run 1---- 794ms

98841 handleOpeningApps dialog

99776 relayout dialog 隐藏

从时间的log来看,我们发现绘制的时间是一致的 (finishDrawingWindow 一个701ms一个 709ms),所以就可以得出了结论,确实是线程运行的时候,这个消息抛出的时间太早,引起这里的隐藏 在系统windowstate这里处理的出现了问题,引发故障。

05

总结:挖掘此问题,主要是要解决,到底我们输入出错后,该如何分析,主要抓住dumpsys信息,看焦点窗口到底在哪个上面,然后再去根据 handleOpeningApps viewVisibility= finishExit in handleAppTransitionReadyLocked

等一些关键log,去推断出逻辑,同时根据代码,去排查,最终锁定问题。

最终我们抽离出来错误代码:

这里差异就是,使用 MainActivity.this.runOnUiThread 和使用view.post的微小差别。 我们看下对应代码:

MainActivity.this.runOnUiThread

可以看到Activity.runOnUiThread 里面,如果不在主线程,直接给主线程post一个消息action。 如果是在主线程,直接运行。我们这里不在主线程,是给主线程post了一个消息。

View.post里面,可以看到如果attachInfo为空,就扔到一个队列里面,后续在dispatchAttachedToWindow回调中才取出来,所以就会将消息向后推迟一会,就是这一会,状态就OK的啦。