1 问题描述
Monkey跑出的无焦点窗口的ANR问题。
特点:
1)、上层WMS有焦点窗口,为Launcher。
2)、native层InputDispacher无焦点窗口,上层为”recents_animation_input_consumer“请求了焦点,但是”recents_animation_input_consumer“最终没有成为焦点窗口,原因是”NO_WINDOW“。
2 log分析
3 复现ANR
从log分析中可以看到发生ANR的场景的上下文:
1)、首先是SystemUI主线程比较卡顿的情况下,这种的我们要复现问题可能需要给SystemUI的绘制加点延迟。
2)、先启动了一个Activity,“com.tct.nxtvision_ui/.LauncherActivity”。
3)、随后屏幕转为了180度,即ROTATION_2。
4)、输入KeyEvent.KEYCODE_RECENT_APPS(312),切换到Launcher的Recents,并且屏幕转为0度,即ROTATION_0。
5)、接着在焦点App切换到Launcher之前,又输入了一个KeyEvent.KEYCODE_BACK(4),并且是第一步启动Activity接收到了,并且调用了finish —— KO!!!后续native层的InputDispacher出现了无焦点窗口的情况。
4 原因分析
4.1 NO_WINDOW代码分析
回到当时看log时,分析ANR出现的直接原因:
这里InputDispatcher侧的焦点窗口从“recents_animation_input_consumer”切走,直接原因为“NO_WINDOW”,查看到具体代码位置,FocusResolver.setInputWindows:
关键的地方有:
大概的内容为,局部变量currentFocus代表的是存储的焦点窗口,而windows代表的是从SurfaceFlinger处传入的最新的可接收输入事件的输入窗口列表,这里继续调用FocusResolver.getResolvedFocusWindow去寻找一个焦点窗口,并且调用FocusResolver.updateFocusedWindow来更新焦点窗口。
继续看FocusResolver.getResolvedFocusWindow:
这里的逻辑主要是调用FocusResolver.isTokenFocusable来判断某一个窗口是否能够作为焦点窗口:
这里要遍历的windows是从SurfaceFlinger发来的最新的输入窗口列表,而token是之前设置的焦点窗口,这个函数的大意是根据最新的输入窗口信息判断之前设置的焦点窗口是否有效。
那么可能有几种情况,即对应Focusability的4个值:
1)、返回NO_WINDOW,说明之前设置的焦点窗口已经不在最新的输入窗口列表里了,即该输入窗口的Layer已经被移除了,或者不满足Layer.needsInputInfo的条件。
2)、返回NOT_FOCUSABLE,说明之前设置的焦点窗口还在最新的输入窗口列表里,但是被设置了NOT_FOCUSABLE这个标志位,不满足作为焦点窗口的条件了。
3)、返回NOT_FOCUSABLE,说明之前设置的焦点窗口还在最新的输入窗口列表里,但是被设置了NOT_VISIBLE,即该Layer已经不可见了,所以不能再作为焦点窗口了。
4)、返回OK,找到了一个符合条件的窗口作为焦点窗口,并且将该窗口保存在传参outFocusableWindow中。
从上面的代码分析可知,焦点窗口从“recents_animation_input_consumer”切走的原因为它对应的Layer已经被移除了,或者不满足Layer.needsInputInfo的条件,继续打开DebugConfig.DEBUG_FOCUS这个开关继续看看log,发现有:
原因为“Window went away”,是该Layer被移除了。
4.2 recents_animation_input_consumer分析
首先看下正常情况下这个Layer的信息为:
它的zOrderRelativeOf就是被transientHide的那个Task。
该Layer(SurfaceControl)在创建INPUT_CONSUMER_RECENTS_ANIMATION对应的Input ConsumerImpl对象中创建:
显示、隐藏和移除的逻辑为:
4.3 “Window went away”原因分析
复现问题后,dumpsys SF的信息,发现:
“recents_animation_input_consumer”对应的Layer仍然存在,那为什么没有遍历到呢?
查看SurfaceFlinger向InputDispatcher更新输入窗口的地方,SurfaceFlinger.updateInputFlinger:
1)、SurfaceFlinger.updateInputFlinger方法的逻辑比较简单,调用SurfaceFlinger.buildWindowInfos构建一个输入窗口列表,然后发给InputDispatcher,那么关键的地方就在于SurfaceFlinger.buildWindowInfos了,它是如果选择哪个Layer可以作为输入窗口的?
2)、SurfaceFlinger.buildWindowInfos的逻辑也很简单,主要是通过Layer.needsInputInfo来判断一个Layer是否能够作为输入窗口的:
应该是主要是判断Layer.hasInputInfo,而“recents_animation_input_consumer”在show的时候已经设置了一个InputWindowInfo了,所以原因应该不是这个。
继续添加log,发现的确如此,“recents_animation_input_consumer”没有发送给InputDispatcher的原因是“recents_animation_input_consumer”这个Layer根本就没有遍历到!
原因则是:
“recents_animation_input_consumer”的相对层级的那个Task已经从Layer层级结构中被移除了,这个Task都遍历不到了,自然“recents_animation_input_consumer”也遍历不到了。
所以这个问题的根本原因是:
1)、在native层,“recents_animation_input_consumer”已经不能作为焦点窗口了,因为transientHide的那个Task已经被移除了。
2)、在上层WMS处,“recents_animation_input_consumer”仍然可以作为一个焦点窗口去请求焦点,没有考虑到transientHide的Task此时是否已经被移除。
再看下WMS处为“recents_animation_input_consumer”请求焦点的逻辑:
主要就是这个成员变量mActiveRecentsActivity:
当Recents界面被调起,就为mActiveRecentsActivity为Launcher对应的ActivityRecord,并且激活“recents_animation_input_consumer”来作为焦点窗口。
比如,当我们点击Recents的时候,会启动一个Transition:
1)、在Transition.onTransactionReady阶段,会调用Transition.handleLegacyRecentsStartBehavior来设置InputMonitor.mActiveRecentsActivity为Launcher对应的ActivityRecord。
2)、在Transition.finishTransition阶段,会将InputMonitor.mActiveRecentsActivity置为null。
在我们复现ANR的场景:
1)、在native层SF,由于和“recents_animation_input_consumer”绑定的那个transientHide的那个Task已经被移除了,所以“recents_animation_input_consumer”就不能作为焦点窗口了。
2)、在上层WMS,因为仍然处于Recents界面,这个Transition一直都不会finish,那么InputMonitor.mActiveRecentsActivity就一直不为空,那么每次走到InputMonitor.updateInputFocusRequest的时候,就会为“recents_animation_input_consumer”请求焦点。
这么看来是InputMonitor.updateInputFocusRequest为“recents_animation_input_consumer”请求焦点的这段逻辑有点问题。
5 在pixel上复现ANR
那我们如何在pixel上复现呢?从以上分析可知这个问题似乎是google原生问题,SystemUI卡顿并非复现该场景的必要条件,毕竟本题中SystemUI卡顿带来的效果的本质是,推迟Launcher称为焦点App的时间,从而让输入KEYCODE_RECENT_APPS后再次输入的KEYCODE_BACK能够被“com.tct.nxtvision_ui/.LauncherActivity”接收到做finish的操作。
那么我们需要让我们模拟的App,在输入KEYCODE_RECENT_APPS切换到Launcher后自动finish,不需要为SystemUI增加延迟,似乎也可以复现“recents_animation_input_consumer”请求不到焦点的情况。
最终果然可行,pixel的Launcher,“com.google.android.apps.nexuslauncher/.NexusLauncherActivity”也可以复现:
finish的代码为:
@Overridepublic void onTopResumedActivityChanged(boolean isTopResumedActivity) {super.onTopResumedActivityChanged(isTopResumedActivity);if (!isTopResumedActivity) {Handler handler = new Handler();handler.postDelayed(new Runnable() {@Overridepublic void run() {finish();}}, 4000);}}
可以看到复现问题的路径更简单了,再次总结一下:
1)、首先是SystemUI主线程比较卡顿的情况下,这种的我们要复现问题可能需要给SystemUI的绘制加点延迟。
2)、写一个Demo App,启动“com.example.demoapp/.MainActivity”,为其设置方向android:screenOrientation=“reversePortrait”,这样它一启动屏幕就转为ROTATION_180(当然用adb命令让屏幕转成180也行,monkey应该就是这么做的,没什么区别)。
3)、输入KeyEvent.KEYCODE_RECENT_APPS(312),这将切换到Launcher的Recents,并且屏幕转为0度,即ROTATION_0。
4)、让“com.example.demoapp/.MainActivity”在切换到Launcher的4s后调用finish —— KO!!!后续native层的InputDispacher出现了无焦点窗口的情况,如果再发一个KeyEvent,我一般发KeyEvent.KEYCODE_BUTTON_C(98),比如就会复现ANR,log就如上面的贴的所示。