Android5.0L退出APP横竖屏切换导致的触摸屏输入(Touch Event)无效(冻屏)问题分析(Key Event仍然有效)
一、问题现象
1、多次进出须要强制横屏的app,比方Real FootBall2015,在退出app的时候会有概率出现退出卡顿。然后TP无法输入的问题。
2、出问题时Power key有响应。
3、此问题同一时候在Driver only上有复现。
Platform:MSM8916
Android版本号:5.0.2L
BuildType:user
系统软件版本号:vA6P+L5P0
系统RAM:1GB
參考机行为:
1、ALTO4.5TMO在相同的简单測试下没有重现此问题,后经分析代码发现是4.4.4KK与5.0.2L在机制上的差别,以下会有详细的对照分析过程。
2、Nexus4和Nexus5在相同的简单測试下没有重现此问题,因没有源代码所以无法Debug和打印log。兴许会尝试获取nexus的源代码以了解它的改动方案。
二、解决方案
通过初步分析、深入分析、对照分析(详细分析过程、关键代码和log在以下会附上)我们清楚的知道了问题发生的原因、流程以及正常情况下的流程。在这个问题中有许多条件都起到了关键作用,终于促成了发生故障的死循环。假设是紧急的备用方案我们能够在不论什么关键条件的环节进行条件推断处理将出问题的这样的情况加以避免,可是假设是长期的终于解决方式我们就要从问题的源头进行解决。
明白一下问题的根本原因:
1、动画的运行依赖VSYNC信号,可是VSYNC信号的到来具有规律性。不到16.66ms不能强行发生
2、WIN DEATH和APP DEATH具有不规律性,在不论什么时间点都有可能发生
3、退出Window须要横屏切换到竖屏。须要冻结屏幕和输入
4、解冻屏幕和恢复输入须要全部Window都Rotate ready
5、出现问题时退出的窗体须要在屏幕和输入没有冻结之前运行退场动画的状态处理。然后才干正常finishExit满足Rotate ready条件
一旦上面的退出窗体没有在冻结屏幕和输入之前运行一次退场动画的状态处理就会成为触发问题死循环的导火索。
针对以上问题的根本原因,我们给出以下解决方式:
1、掐断问题的导火索
在不正确冻结和动画进行大机制上的同步改动的前提下,对已经冻结VSYNC才来运行退出窗体的退场动画的情况下进行状态处理,由于已经冻结,所以退场动画不须要再运行,须要做的是清除这个待运行的动画。确保运行finishExit的正常清理操作,从而满足Rotate ready条件,让系统的正常机制来进行恢复处理。AOSP的这块代码已经考虑到了VSYNC先到来可是还有窗体动画的一种情况,可是没有考虑到退场动画的情况。所以在不做大影响的改动下我们在AOSP原来的机制上新增一种退场动画的条件推断,达到解决这个问题的目的。
2、原生代码和凝视
通过以上代码能够清楚的看到假设屏幕已经冻结okToDisplay就会为false,因此if里面的代码主体就不会被运行到,因此动画的状态就不会被更新,所以以下就会直接返回。另外我们还能够看到另一个否则条件以及它的凝视。清楚的说明了假设屏幕已经冻结可是另一个窗体动画(不是退场动画)。那么就清除这个动画,确保运行以下的清理代码,可是很遗憾这个条件没有把当前这个问题的退场动画包括,所以就无法运行正常的finishExit的清理代码,finishExit中的工作也至关重要。详细代码例如以下:
通过以上代码我们能够看到finishExit中主要做了几个关键动作,首先finishExit当前窗体的全部子窗体,然后将窗体添加待销毁surface的列表中。同一时候将mExiting置为false,这个false很重要,直接影响我们之前分析的apptoken的mDeferRemoval能否被正常删除。同一时候将窗体添加待删除的列表中,这个是真正删除的列表。
3、终于改动的代码方案
我们须要改动一行代码在原来的机制流程上比較完美的解决这个问题,详细代码例如以下:
假设屏幕和输入已经冻结。可是窗体的退场动画还没有在运行,那么将正常运行动画的状态置为true,确保正常运行以下的finishExit。终于恢复正常流程。解决这个问题。
三、问题初步分析
以Idol347出问题时候的一份典型log为例,发现出问题时log中打出大量例如以下信息:
通过查看代码,发现上面的log是在InputDispatcher的dispatchOnceInnerLocked中打出来的,详细例如以下:
通过以上代码我们能够发现,是mDispatchFrozen条件成立才打印出的这句log。
mDispatchFrozen条件是什么时候被置成true的?代码中找答案:
通过上面的代码我们能够发现是在setInputDispatchMode函数中设置的mDispatchFrozen,接下来继续看哪里调用的setInputDispatchMode,通过查看代码发现是从WindowManagerService一路调下来的,中间经过了JNI。详细例如以下:
什么场景下会让WindowManagerService冻结的输入?通过查看代码和添加log,我们能够查看出问题时详细的调用栈信息:
通过详细调用栈我们能够发现是ActivityManagerService在处理app死亡通知时,会resume下一个app,在resume的过程中会去调用WindowManagerService的方法检查是否须要转屏。假设须要转屏则调用startFreezingDisplayLocked冻结显示,在冻结显示的过程中会冻结输入:
知道了冻结输入的场景。接下来另一个更重要的问题。什么场景下会让WindowManagerService恢复正常输入?有冻结就有解冻,继续查看代码和调用栈信息:
从调用栈信息中我们能够发如今处理app died通知并resume下一个app的过程中会调用解冻显示,解冻显示的过程中会解冻输入。可是从log中能够看到,出问题时解冻显示函数在还没有走到解冻输入的时候就由于mWindowsFreezingScreen条件为true而返回了,因此输入没有恢复正常。初步分析到这里已经定位到第一个问题点:mWindowsFreezingScreen为true导致不能正常解冻而恢复输入。可是mWindowsFreezingScreen条件为什么为true?难道仅仅有这一个地方会去解冻恢复吗?带着这两个问题我们继续分析。
四、深入分析问题
经过初步我们定位到了第一个问题点。同一时候也产生了两个问题。接下来我们继续深入分析以期能到找到答案和问题的根本原因。
1、mWindowsFreezingScreen条件为什么为true?
2、还有什么地方会解冻恢复正常输入?
通过查看代码发现mWindowsFreezingScreen有两个地方置为true,一个地方置为false:
通过以上代码我们能够知道在运行updateRotationUncheckedLocked的时候假设须要转屏则会会将mWindowsFreezingScreen置为true一次,然后每次调用makeWindowFreezingScreenIfNeededLocked的时候假设屏幕已经frozen,也会将mWindowsFreezingScreen置为true。而将mWindowsFreezingScreen置为false的地方仅仅有一个,置为false的同一时候也会解冻输入。
这也间接回答了我们的第二个问题,除了初步分析中的第一个解冻输入的地方,另一个解冻输入的地方。那就是performLayoutAndPlaceSurfacesLockedInner:
这个函数是WindowManagerService很重要的一个函数,依据名字我们能够知晓其功能的一二,他里面主要运行布局、计算、窗体的移除以及动画的调度等各种状态管理,是调用频率很高的一个函数。仅仅要窗体状态有不论什么的变化都会运行到这里。到这假设mWindowsFreezingScreen想要被置为false,还须要满足一个条件,那就是mInnerFields.mOrientationChangeComplete必须为true,我们继续追踪mInnerFields.mOrientationChangeComplete何时被置为true,发现仅仅有一个地方mInnerFields.mOrientationChangeComplete会被置为true,详细代码例如以下:
分析到这能够看到与Animation開始产生关系了,继续追踪调用关系。发现copyAnimToLayoutParamsLocked是在WindowAnimator的animateLocked中调用的,而animateLocked是由VSYNC信号来了之后由Choreographer的FrameDisplayEventReceiver调用的。详细调用栈例如以下:
在调用copyAnimToLayoutParamsLocked之前,animateLocked会先调用updateWindowsLocked去更新全部应用的动画。包括正在退出和已经删除的应用,然后还会调用WindowStateAnimator的prepareSurfaceLocked去做对应的状态计算,详细代码例如以下:
在运行updateWindowsLocked时会调用WindowStateAnimator的stepAnimationLocked,这个函数在当前这个问题中有很关键的作用,以下会着重介绍。
由于ActivityManagerService在处理app died的时候并没有与WindowManagerService处理Window died和运行动画进行同步,因此就有可能出现Window died的退场动画还没有来得及等到下一个VSYNC(16.666ms一次)运行一次动画操作,就被ActivityManagerService在resume下一个须要转屏的应用时冻结屏幕和输入,在下一个VSYNC来了之后去运行window died的退场动画时发现屏幕已经冻结,从而不能正常finishExit的window而直接返回,成为这个问题的一个最关键的点。
Window died的关键处理代码例如以下:
performLayoutAndPlaceSurfacesLocked终于会调用到performLayoutAndPlaceSurfacesLockedInner,然后就会运行窗体大小的计算和相关状态更新,当中影响此问题很关键的操作是调用updateResizingWindows:
由于window dead,所以window和可视内容以及大小发生了变化。因此会调用makeWindowFreezingScreenIfNeededLocked,这个函数中会推断屏幕是否已经冻结。假设已经冻结则会将mInnerFields.mOrientationChangeComplete一直置为false,尽管WindowAnimator会调用copyAnimToLayoutParamsLocked将mInnerFields.mOrientationChangeComplete置为true,可是由于运行copyAnimToLayoutParamsLocked之后仍然须要调用requestTraversalLocked去运行performLayoutAndPlaceSurfacesLocked。所以会被makeWindowFreezingScreenIfNeededLocked再次置为false,事实上performLayoutAndPlaceSurfacesLocked的运行过程中会对Window的内容和大小变化进行更新,正常情况下运行makeWindowFreezingScreenIfNeededLocked的条件不会一直满足。详细代码例如以下:
可是当前这样的情况比較特殊。由于Window已经结束。所以调用mClient.resized会发生RemoteException,导致上面代码中的状态不能被置为false。从而导致调用makeWindowFreezingScreenIfNeededLocked的条件一直满足。终于使WindowManagerService不能解冻屏幕和恢复输入。一旦屏幕先冻结,这里会与WindowStateAnimator的stepAnimationLocked的处理一起形成一个不能解冻和恢复正常输入的死循环。
死循环在哪里?
1、Window退出调用WindowManagerService的removeWindowLocked
2、removeWindowLocked会运行退场动画,并调用performLayoutAndPlaceSurfacesLocked进行一次计算和状态处理。并将动画进行调度处理,放入下一个VSYNC的处理列表中,由于动画还没有被运行处理所以mInnerFields.mOrientationChangeComplete不为true,因此mWindowsFreezingScreen也不会被置为false,屏幕和输入不会被解冻和恢复。
3、ActivityManagerService接收到app died的通知之后resume下一个app,下一个app与当前结束的这个app的orientation不一样,触发冻结屏幕和输入。
4、VSYNC到来,运行动画的相关操作。由于屏幕已经被冻结,所以正在退出的Window不能运行动画操作而直接返回,导致finishExit不能被运行,终于Window不会被正常删除。运行copyAnimToLayoutParamsLocked将mInnerFields.mOrientationChangeComplete置为true。然后调用requestTraversalLocked发送运行下一次performLayoutAndPlaceSurfacesLocked的消息到消息队列中。
5、performLayoutAndPlaceSurfacesLocked运行。调用updateResizingWindows。由于退出的Window没有被finishExit,并且运行reportResized更新窗体大小和内容状态的过程中由于Window已经退出,所以调用mClient.resized运行IPC(跨进程调用)时发生RemoteException。导致关键状态值没有被置位清空,所以运行updateResizingWindows的过程中会由于Window的状态一直满足条件而调用makeWindowFreezingScreenIfNeededLocked。由于此时窗体已经被冻结。所以会将mInnerFields.mOrientationChangeComplete一直置为false,因此不会将mWindowsFreezingScreen置为false和调用stopFreezingDisplayLocked解冻屏幕和恢复输入。接着会调用scheduleAnimationLocked将下一次动画调度到VSYNC的列表中。
6、下一次VSYNC到来,反复第四步和第五步构成不能解冻屏幕和恢复输入的死循环
五、KK4.4.4与L5.0.2的机制差别
1、L5.0.2新增条件mDeferRemoval
接收到app的dead通知之后。ActivityManagerService会调用WindowManagerService的removeAppToken。详细代码和调用关系例如以下:
在removeAppToken的过程中。KK4.4.4与L5.0.2有一些差别,L5.0.2新添加了一个条件mDeferRemoval。为了这个处理这个条件L5.0.2新添加一些代码来一起完毕这个机制特性。关键详细代码例如以下:
KK4.4.4的关键详细代码例如以下:
mDeferRemoval这个条件会影响mExitingAppTokens中apptoken的删除和与apptoken关联的Window的删除,并且这个条件与正在运行动画或者正在退出也强相关,通过上面的分析和代码我们知道发生故障时WindowManagerService存在一个死循环。因此在运行performLayoutAndPlaceSurfacesLocked的过程中调用checkForDeferredActions时,stack.isAnimating()条件会一直满足,由于有正在退出的窗体还没有finishExit,因此不会做mExitingAppTokens的删除,所以apptoken会一直存在,与这个apptoken关联的Window也会一直存在。
KK4.4.4没有mDeferRemoval这个条件,所以会在performLayoutAndPlaceSurfacesLocked的过程中直接删除apptoken。
另外KK4.4.4在相同先冻结屏幕再来VSYNC运行动画的情况下Window相同不会被finishExit而保留在WindowList中,可是由于KK4.4.4没有mDeferRemoval的机制,所以在rebuildAppWindowListLocked的时候会将不能正常被finishExit可是它的apptoken已经从task和exitingAppTokens中删除的窗体删除,同一时候MTK还加了一个patch用来彻底remove Window防止窗体泄露,详细代码例如以下:
所以尽管在退出强制横屏的窗体进入launcher不能正常finishExit的时候,在不进入其它app的情况下能够通过adb shell dumpsys |grep “game”看到这个窗体还在,可是当你进入其它app窗体的时候会调用handleAppTransitionReadyLocked,然后再调用rebuildAppWindowListLocked将其删除,这里你在运行上面的命令就看不到不能finishExit的窗体了,原因就是上面分析的机制和代码所致。
2、L5.0.2使用带有虚拟按键的NavigationBar
由于L5.0.2使用带有虚拟按键的NavigationBar,所以在退出强制横屏app的窗体回到Launcher时。与KK4.4.4手机使用实体按键在config change时的布局条件不同。L5.0.2在布局时由于从全屏显示到退出到launcher会发生rotate引起config change,所以会运行一次布局。同一时候须要更新显示NavigationBar区域。所以在运行computeFrameLw过程中mContentFrame会发生变化,进而引起mContentInsets的变化,终于win.setInsetsChanged()条件满足。由于前面说到的运行reportResized更新窗体大小和内容状态的过程中由于Window已经退出,所以调用mClient.resized运行IPC(跨进程调用)时发生RemoteException,导致关键状态值没有被置位清空。所以这里也是相同情况,布局条件会因此一直满足,performLayoutLockedInner中详细的关键代码例如以下:
win.mLayoutSeq这个条件会影响到窗体freezing状况的保持以及mInnerFields.mOrientationChangeComplete状态的置位,详细的关键代码例如以下:
运行窗体rotate之后的布局參数log例如以下:
圈红的參数中cf是contentframe的矩形大小,它会与frame做计算。得出一个ci,由于从全屏横屏到launcher的变化。所以NavigationBar会被计算布局和刷新,因此ci与上次不同w.setInsetsChanged();满足为true,同一时候w.mContentInsetsChanged为true。
KK4.4.4由于没有使用NavigationBar所以上面的条件不会满足。通过实验我将KK4.4.4的NavigationBar打开。相同情况下上面的条件也会满足,可是KK4.4.4依旧不会冻屏。由于我參考的KK4.4.4有MTK的patch,添加一个条件:mDisplayFrozenTimeout,当WINDOW_FREEZE_TIMEOUT之后,mDisplayFrozenTimeout会被置为true,所以makeWindowFreezingScreenIfNeededLocked函数中保持屏幕冻结状态的代码不会被走到,详细例如以下:
另外我參考的KK4.4.4还有一处改动也会直接影响不会发生此问题,在WindowAnimator中加了一处推断转屏动画结束并解冻恢复输入的代码,详细例如以下:
以上这些差别让我參考的MTK的KK4.4.4不会出现冻屏不能输入的问题。
六、兴许动作和其它相关问题
1、尝试获取Nexus的源代码以了解它的解决方式
2、在Window Freezing timeout的时候强制解冻和恢复输入的暂时方案以及它会引起的问题:
引起的第一个问题:为什么每次退出强制横屏的应用到竖屏都会Window Freezing timeout?
分析:由于正在退出的窗体没有被finishExit。所以它会一直存在并影响mInnerFields.mOrientationChangeComplete条件一直为false。所以不会正常运行解冻恢复的代码以及app transition的代码,终于导致超时后强制解冻和恢复输入,关键代码例如以下:
引起的第二个问题:为什么在同一个应用运行转屏动作也会等到Window Freezing timeout才进行转屏?
与第一个问题是相同原理,由于有一个没有被finishExit的窗体,所以stopFreezingDisplayLocked不会被及时的正常运行,而stopFreezingDisplayLocked中会进行ScreenRotationAnimation的dismiss操作,dismiss会将ScreenRotationAnimation给start起来,然后进行ScreenRotationAnimation,关键的详细代码例如以下:
Analyzed by vincent.song from SWD2 Framework team.
vincent.song@tcl.com
201504231130