一、问题现象
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