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

来源:互联网 发布:postman post传递json 编辑:程序博客网 时间:2024/06/06 16:28

image

问题描述:

某个界面启动后,上面的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方法里面。

image

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

image

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

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

Input Dispatcher State:

image

这里可以找到input可以传递的一个窗口列表
这里关键的几个信息:

FocusedApplication :

当前焦点app

FocusedWindow: name=’Window{f8c1e72 u0 com.codegg.fba/com.codegg.fba.activity.romListActivity}’

当前focus的窗口信息

后面紧跟着一堆窗口列表:

image

列表的一些信息:

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]

此窗口的可点击区域

然后我们查找代码,去看下输入服务那边,是如何判断发送给谁的呢?

image

我们找到

InputDispatcher.cpp

findTouchedWindowAtLocked

,可以看到,这里关键的信息是:

windowInfo->visible

,由于我们排列顺序是从前往后,因此第一个遍历到对话框窗口的时候,发现

windowInfo->visible=True

,因此系统会将触摸消息,发送给这个窗口,也就是对话框。然而,实际上对话框在apk这边,已经是隐藏状态,同时自身也不消耗触摸事件,因此导致事件一直发给一个隐藏的窗口,引出问题。

01

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

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

这里会将窗口赋值进来。然后我们全局搜索

setInputWindows

,最终在

InputMonitor.java

updateInputWindowsLw

方法里面,锁定了关键逻辑。

updateInputWindowsLw

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

image

这里有个方法

isVisible = child.isVisibleLw();

会去更新显示状态,我们之前看到,就是这个变量是Ture,导致系统认为我们的对话框是可见,引出的问题。

于是我们的重心,转移到了这里,我们看下代码:

image

我们主要关心

!mAnimatingExit && !mDestroying

这两个值(

其他本身也是要关注,但是因为已经跟过,知道他们不变,所以去掉了那些无关的变量

02

当前窗口的信息,这些变量如何得知的呢?我们来看个推演过程,我们之前使用adb shell dumpsys的文档,打开,

我们通过

Input Dispatcher State

,找到了当前focus的是romListActivity,但是显示的有两个,一个是activity的主窗口,一个是对话框的窗口,对话框的layer比activity的layer高,因此它优先得到了触摸响应。
具体对话框的信息如下:

image

我们使用这里的

name=’Window{1781b28 的1781b28

,在文本中搜索,可以找到window的详细信息:

image

mHasSurface=true
mPolicyVisibility =true
mAttachedHidden=false
mAnimatingExit=false
mDestroying=false
mIsWallpaper=false
mWallpaperVisible=xxx

关于这些值怎么算出来的,是通过这里的dump信息,我们找到windowState.java的dump,我们调用的dumpsys命令,会走到这里,

image

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

03

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

我这里尝试使用demo来测试,写了如下代码:

image

也就是把出问题的那段逻辑,搬出来独立测试下,发现没有问题,这样子我们就可以进行对比了。然后通过

dumpsys

之后,发现了关键数据,在dump里面,出现了一些数据:

image

我们发现,这里的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方法中。

image

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

image

搜索得到一些数据,我们可以使用后面 的

WindowStateAnimator{91b6679

这里的

91b6679

便是地址,那么我们从dumpsys里面,找到当前dialog窗口的动画地址,

91b6679

image

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

我们继续查找,使用

91b6679

,发现了一段异常逻辑。

image

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

EXITING

}

log中的

addInputWindowHandle

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

image

也就是

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

方法里面。

image

同时我们在

handleAppTransitionReadyLocked

方法中,看到如下语句:

image

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

然后我们在

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的显示属性。

image

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

image

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

image

然后这里的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代码里面

image

于是,我们又需要去检查,出错的时候,为什么

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,去推断出逻辑,同时根据代码,去排查,最终锁定问题。

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

image

这里差异就是,使用

MainActivity.this.runOnUiThread

和使用

view.post

的微小差别。
我们看下对应代码:

MainActivity.this.runOnUiThread

image

可以看到

Activity.runOnUiThread

里面,如果不在主线程,直接给主线程post一个消息action。
如果是在主线程,直接运行。我们这里不在主线程,是给主线程post了一个消息。

image

View.post

里面,可以看到如果attachInfo为空,就扔到一个队列里面,后续在

dispatchAttachedToWindow

回调中才取出来,所以就会将消息向后推迟一会,就是这一会,状态就OK的啦。

技术在于灵活使用,才能发挥巨大作用。

本文完。喜欢本文,分享给别人,喜欢代码GG,扫二维码,关注代码GG之家。

image

阅读全文
0 0
原创粉丝点击