问题描述
Monkey跑出的Camera发生ANR的问题,其实跟Camera无关,任意一个App都会在此场景下发生ANR,场景涉及到Launcher的RecentsActivity界面,和transientLaunch相关。
1 log分析
看问题发生的场景:
1、Camera App的相关界面CameraLauncher拿到焦点,此时正常。
2、Monkey输入了一个keycode为312的KeyEvent(KEYCODE_RECENT_APPS)调起了RecentsActivity,这是一次transientLaunch,可以看到CameraLauncher的生命周期没有发生变化,以及最终是“recents_animation_input_consumer”拿到了焦点,但是奇怪的一点是CameraLauncher的ActivityRecord被设置不可见了(wm_add_to_stopping这条log)。
3、接着Monkey应该是又输入了一个MotionEvent,然后与“recents_animation_input_consumer”发生了交互,点击了Camera对应的Recents缩略图,所以又调起了CameraLauncher,:
3.1)、刚开始,发生了一次relayoutWIndow,但是CameraLauncher对应的ActivityRecord的可见性还没有被设置为true,所以它的窗口不满足作为焦点窗口的条件,这导致了后续的DisplayContent.updateFocusedWindowLocked没有办法将它的窗口设置为焦点窗口。
3.2)、接着“wm_set_resumed_activity”之后,CameraLauncher重新变为resume,其ActivityRecord的可见性也被设置为true,但是由于CameraLauncher对应的WindowState的可见性始终没有发生变化,导致了后续再走relayoutWindow的时候,没有办法调用DisplayContent.updateFocusedWindowLocked去更新WMS的焦点窗口,进而无法为CameraLauncher请求焦点,最终结果就是上层WMS侧以及native层InputDispatcher侧都没有焦点窗口。
其中有两点比较奇怪:
1、“05-30 18:30:28.853”时间点,输入了KeyEvent,KEYCODE_RECENT_APPS,但是调起的是“com.tcl.android.quickstep.RecentsActivity”,并非“TclQuickstepLauncher”,这是第一个问题点。
2、从log上看启动了“com.tcl.android.quickstep.RecentsActivity”后“com.android.camera.CameraLauncher”的生命周期没有发生变化,并且最终获取到焦点的是“recents_animation_input_consumer”,而非“com.tcl.android.quickstep.RecentsActivity”,说明这次“com.tcl.android.quickstep.RecentsActivity”的启动是一次瞬态启动,transientLaunch,这个行为应该是Launcher那边控制的,我本地启动“com.tcl.android.quickstep.RecentsActivity”的话,并不是瞬态启动,获取到焦点的也是“com.tcl.android.quickstep.RecentsActivity”,而不是“recents_animation_input_consumer”,这是第二个问题点。
2 复现步骤
最终和Launcher的同事确认后知道,这个场景下用的非我们默认的Launcher,而是另外一个Launcher,类似于在pixel上装了一个三方Launcher,因此点击Recents键(或者输入312)会调起RecentsActivity。
那么在联系ANR发生的上下文,我们已经可以知道该ANR发生的具体步骤了:
1、设置一个三方Launcher为默认Launcher,如NovaLauncher。
2、启动Camera(其实任意一个App都行,我们分析的ANR场景是Camera):
adb shell am start -n com.tcl.camera/com.android.camera.CameraLauncher
3、输入KEYCODE_RECENT_APPS(前提必须是RecentsActivity之前没有启动过):
adb shell input keyevent 312
4、选择近期任务列表中的Camera,即可复现无焦点窗口的情况。
5、再随便输入一个KeyEvent,即可触发ANR计时:
adb shell input keyevent 98
另外pixel上是没这个问题的。
3 问题分析
3.1 瞬态启动transientLaunch和瞬态隐藏transientHide介绍
凭借我们对transientLaunch的了解,一个最不寻常的点就是,启动RecentsActivity是一次瞬态启动,但是为什么CameraLauncher被计算为不可见了?
首先大概说一下我个人对于这个瞬态启动的理解,我现在随便打开一个App,比如Camera,接着点击Recents键,启动Launcher的Recents界面(现在Launcher的Home界面和Recents界面都是同一个Activity,不像之前点击Recents键后会启动另外一个界面RecentsActivity了。但是对于第三方的Launcher,比如NovaLauncher,点击Recents键后还是会启动RecentsActivity,这个是Launcher那边的逻辑,具体的我也不是很了解),此时Launcher的Recents界面的这次启动就会被认为是transientLaunch,瞬态启动。个人猜测加入transientLaunch的逻辑应该是google认为用户调起Recents界面的原因是想在Recents界面上选择另外的App进入,不会在Recents界面停留太长时间,因此就把调起Recents界面的行为定义为transientLaunch。
相应的,transientLaunch的特点就是,被transientLaunch的TaskA所遮挡的TaskB,不会被认为是不可见的,即经过transientLaunch后,TaskA跑到了TaskB的上面,但是TaskB还是会被认为是可见的。回到我们的例子,在Camera界面下点击Recents键启动Launcher的Recents界面,Recents界面就会被认为是瞬态启动的,而Camera对应的Task就会被认为是transientHide,瞬态隐藏的,也就是说它只是短暂的被transientLaunch的App遮挡了(即Recents界面),不应该就直接认为它是不可见的,那么它的Activity的生命周期也不会发生任何变化,如log:
可以看到,只是Launcher的Activity的生命周期从STOP变为RESUME,Camera的Activity的生命周期并没有变化。
如果我们在Recents界面重新选择Camera回到Camera,在整个过程中(Camera -> Recents -> Camera)Camera的可见性和生命周期是不会发生任何变化的,减少了很多不必要的工作,因为如果没有transientLaunch的逻辑的话,Camera会从可见变为不可见再变为可见,它的生命周期就会从RESUMED -> PAUSED -> STOPPED -> STARTED -> RESUMED,而在transientLaunch的逻辑下,整个过程中,Camera的Activity的生命周期状态一直都是RESUMED,不会发生变化,我猜这可能就是加入transientLaunch的意义。
但是如果我们在Recents界面没有选择Camera进入,而是选择另外一个App,比如Contacts,这种情况下,Launcher的Recents界面已经不在前台了,那么瞬态启动就结束了,Camera的Task的可见性就会变为false。
从上面可以看到,transientLaunch逻辑下,我们把Camera的Task的可见性判断放在更后面的时间点,即从Recents界面离开的时候,而非从Camera界面离开进入Recents界面的时候:
1)、如果从Recents界面回到了Camera,那么Camera的可见性保持为可见,即整个过程中Messge的可见性没有发生变化。
2)、如果从Recents界面进入另外的App,如Contacts,那么Camera的可见性才会从可见变为不可见。
transientLaunch的内容大概就啰嗦这么多,接着看下它作用的地方,在以下代码,计算Task可见性的地方,TaskFragment.getVisibility:
在正式计算Task的可见性之前,对这个Task进行判断,如果它被transientHide,那么直接返回TASK_FRAGMENT_VISIBILITY_VISIBLE,即认为瞬态隐藏的Task是可见的,也即这里的注释,保持transient-hide的根Task为可见,对于非根Task的Task则继续遵守一般规则。
这里判断Task是否是瞬态隐藏的,调用的是TransitionController.isTransientHide:
继续调用了Transition.isTransientHide:
Transition的成员变量mTransientHideTasks定义为:
即保存了因为transientLaunch启动而被遮挡的Task。
顺便看下其成员变量mTransientLaunches,保存了瞬态启动的那个ActivityRecord以及restore-below的Task(这个restore-below不知道怎么翻译,应该是和transientHide那个Task相关,“处于transientLaunch之下的可恢复的Task”)。
向Transition.mTransientHideTasks中添加Task的地方只有一处,在Transition.setTransientLaunch,同样也是唯一一处的向mTransientLaunches添加数据的地方:
1)、向Transition.mTransientLaunches添加键值对<ActivityRecord, Task>,这个传参activity就是瞬态启动的那个ActivityRecord。
2)、如果restoreBlow不为null,那么获取到传参activity的根Task,然后获取到这个根Task的父容器,也就是TaskDisplayArea,接着进行对TaskDisplayArea中的所有Task进行遍历,如果有Task请求可见,那么说明这个Task在瞬态启动之前是可见的,那么我们就把这个Task加入到Transition.mTransientHideTasks中,表示这个Task的可见性即将被瞬态启动影响,后续在TaskFragment.getVisibility中继续保持其为可见。
逻辑还是比较简单的,唯一要注意的是,如果传参restoreBelow为null,那么我们就无法为Transition.mTransientHideTasks添加被瞬态隐藏的Task,其实这里就是问题发生的原因,根据复现ANR的步骤去操作,这里传入的restoreBelow为null,Camera的Task无法被添加到Transition.mTransientHideTasks,导致了Camera的Task无法被认为是瞬态隐藏的,所以Camera的相关ActivityRecord也被认为是不可见的。
为了知道为什么传入的restoreBelow为null,我们需要分析一下这个方法的调用情况。
Transition.setTransientLaunch方法被调用的地方也只有一处,在TransitionController.setTransientLaunch:
从这里的逻辑我们能看到,只有处于收集阶段的Transition才能记录瞬态启动相关的ActivityRecord以及Task。
TransitionController.setTransientLaunch被调用的地方有两处:
后面又经过添加log以及打断点后,大概明白Launcher那边是如何操作的了,这里大概说明一下。
3.2 TaskAnimationManager.startRecentsAnimation
起点在Launcher的TaskAnimationManager.startRecentsAnimation:
首先调用ActivityOptions.setTransientLaunch将本次启动标记为瞬态启动:
这里的注释对瞬态启动也解释的很清楚了,这个方法是一个用于设置活动启动是否为瞬态操作的方法。如果设置为瞬态操作,它将不会导致现有Activity的生命周期更改,即使它会遮挡它们(例如,被此Activity遮挡的其他Activity将不会被pause或stop,直到启动被提交)。因此,它将立即启动,因为它不需要等待其他生命周期的演变。
我们主要看这个ActivityOptions是如何传递的。
3.3 SystemUiProxy.startRecentsActivity
继续调用SystemUiProxy.startRecentsActivity:
这里的mRecentTasks是IRecentTasks类型的,因此调用的是定义在RecentTasksController.java中的IRecentTasksImpl的startRecentsTransition方法:
然后继续调用了RecentsTransitionHandler.startRecentsTransition方法:
两个点,一是创建一个WindowContainerTransaction对象,调用WindowContainerTransaction.setPendingIntent将这个Bundle传入:
二是调用Transitions.startTransition从WMShell侧发起一个Transition。
中间过程我们就不说了,最终会走到WindowOrganizerController.applyHierarchyOp中。
3.4 WindowOrganizerController.applyHierarchyOp
我们主要看对HIERARCHY_OP_TYPE_PENDING_INTENT这个类型的处理(对应之前调用的WindowContainerTransaction.setPendingIntent):
大致的流程为:
1)、通过ActivityStarterController.startExistingRecents调用TransitionController.setTransientLaunch:
如果ActivityStarterController.startExistingRecentsActivity返回了false,那么继续调用ActivityManagerInternal.waitAsyncStart。
2)、调用ActivityStarter.startActivityInner,来创建RecentsActivity对应的ActivityRecord和Task。
3)、启动RecentsActivity完成后,通过ActivityStarter.handleStartResult调用TransitionController.setTransientLaunch:
接下来分别分析。
3.4.1 ActivityStarterController.startExistingRecents
再回顾一下我们复现问题的场景,需要保证之前RecentsActivity还没有启动过,log为:
看到走到ActivityStarterController.startExistingRecents的时候,RecentsActivity对应的Task还没有创建,那么就会因为在TaskDisplayArea中找不到ACTIVITY_TYPE_RECENTS类型的Task而提前返回false,不会继续调用TransitionController.setTransientLaunch,如以下代码展示的那样:
而一旦我们启动过RecentsActivity,那么它所在的Task就会存在于TaskDisplayArea中,后续我们再次点击Recents键启动RecentsActivity的时候就没有问题了。
出现问题的场景下,我们知道这里返回了false,那么就会继续调用ActivityManagerInternal.waitAsyncStart,最终是通过ActivityStarter.handleStartResult调用了TransitionController.setTransientLaunch。
3.4.2 ActivityStarter.handleStartResult
这个流程下,是先调用ActivityStarter.startActivityInner,创建了RecentsActivity对应的ActivityRecord和Task。
启动RecentsActivity完成后,在ActivityStarter.handleStartResult中尝试调用TransitionController.setTransientLaunch,log为:
ActivityStarter.handleStartResult代码为:
根据之前的分析,这里我们知道isTransientLaunch的条件我们是满足的,所以会继续调用TransitionController.setTransientLaunch,但是由于这里传入的mPriorAboveTask是null,所以最终仍然无法将Camera对应的Task标记为瞬态隐藏的。
ActivityStarter.mPriorAboveTask定义为:
注释的大概意思是,mPriorAboveTask是启动Activity前的位于targetTask(启动的这个Activity所在的Task)之上的Task,如果这个Activity启动在一个新的Task中(即targetTask为null),或者targetTask已经处于前台了,那么ActivityStarter.mPriorAboveTask为null。
再看下ActivityStarter.mPriorAboveTask是如何计算的,在ActivityStarter.startActivityInner中:
凭我们对这个方法的了解,知道了如果要启动的这个Activity如果是启动在一个新的Task中,那么这里的局部变量targetTask就为null,那么就不会为ActivityStarter.mPriorAboveTask赋值,符合ActivityStarter.mPriorAboveTask的注释描述。
正好我们复现ANR的场景,也是RecentsActivity第一次启动,需要为其创建一个ACTIVITY_TYPE_RECENTS类型的Task,所以在这个流程下,ActivityStarter.mPriorAboveTask就是null,那么传入TransitionController.setTransientLaunch的restoreBelowTask也是null,最终也不会将Camera对应的Task标记为瞬态隐藏。
3.5 问题总结
总结一下,在整个过程中,我们是有两个地方有机会将Camera对应的Task标记为瞬态隐藏的,即WindowOrganizerController.applyHierarchyOp方法中的这两段:
但是实际上这两个地方都失败了:
1)、ActivityStarterController.startExistingRecents,需要为找到一个RecentsActivity找到一个ACTIVITY_TYPE_RECENTS类型的Task,而RecentsActivity是第一次创建,所以找不到这么一个Task,因此最终没有调用TransitionController.setTransientLaunch。
2)、ActivityStarter.handleStartResult,如果RecentsActivity是第一次创建,那么不会为ActivityStarter.mPriorAboveTask进行赋值,那么最终传入TransitionController.setTransientLaunch的restoreBelowTask就是null,Camera对应的Task还是不会被标记为瞬态隐藏。
从以上分析能够看出,在现在的逻辑下,如果瞬态启动的这个Activity是第一次启动,那么是不会将任何一个Task标记为瞬态隐藏的,这个肯定是不对的,是google的逻辑有问题。
3.6 解决方案
经过以上总结,这个问题是google原生问题,那么pixel上应该也有此问题了?
我本地在pixel上安装了一个NovaLauncher后,按照我们稳定复现ANR的步骤去操作,但是发现pixel没问题,那肯定是google已经修复这个问题了,反编译pixel的services.jar,果然如此,在Transition.setTransientLaunch:
如果传入的restoreBelow为null,那么就用传入的activity的根Task,这样处理的确能保证瞬态启动后,之前可见的Task可以被正确标记为瞬态隐藏的。
对应的google patch为:
3ceb2568736d873ab0a9ebaad40056d908662cc3 - platform/frameworks/base - Git at Google (googlesource.com)
单靠Transition.java这个修改就可以解决了:
不过保险起见,还是整个patch一起合入,pixel的services.jar里也已经包含了整个patch。