美文网首页
android调用dialog.hide()引起的输入事件派发错

android调用dialog.hide()引起的输入事件派发错

作者: 代码GG陆晓明 | 来源:发表于2017-11-16 09:49 被阅读84次
    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

    相关文章

      网友评论

          本文标题:android调用dialog.hide()引起的输入事件派发错

          本文链接:https://www.haomeiwen.com/subject/haokvxtx.html