问题描述:
某个界面启动后,上面的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高,因此它优先得到了触摸响应。
具体对话框的信息如下:
我们使用这里的
name='Window{1781b28 的1781b28
,在文本中搜索,可以找到window的详细信息:
imagemHasSurface=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了一个消息。
View.post
里面,可以看到如果attachInfo为空,就扔到一个队列里面,后续在
dispatchAttachedToWindow
回调中才取出来,所以就会将消息向后推迟一会,就是这一会,状态就OK的啦。
技术在于灵活使用,才能发挥巨大作用。
本文完。喜欢本文,分享给别人,喜欢代码GG,扫二维码,关注代码GG之家。
image
网友评论