【问题分析】WMS无焦点窗口的ANR问题 + transientLaunch介绍【Android 14】

2024-06-07 21:20

本文主要是介绍【问题分析】WMS无焦点窗口的ANR问题 + transientLaunch介绍【Android 14】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

问题描述

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。

这篇关于【问题分析】WMS无焦点窗口的ANR问题 + transientLaunch介绍【Android 14】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1040329

相关文章

Python Jupyter Notebook导包报错问题及解决

《PythonJupyterNotebook导包报错问题及解决》在conda环境中安装包后,JupyterNotebook导入时出现ImportError,可能是由于包版本不对应或版本太高,解决方... 目录问题解决方法重新安装Jupyter NoteBook 更改Kernel总结问题在conda上安装了

pip install jupyterlab失败的原因问题及探索

《pipinstalljupyterlab失败的原因问题及探索》在学习Yolo模型时,尝试安装JupyterLab但遇到错误,错误提示缺少Rust和Cargo编译环境,因为pywinpty包需要它... 目录背景问题解决方案总结背景最近在学习Yolo模型,然后其中要下载jupyter(有点LSVmu像一个

解决jupyterLab打开后出现Config option `template_path`not recognized by `ExporterCollapsibleHeadings`问题

《解决jupyterLab打开后出现Configoption`template_path`notrecognizedby`ExporterCollapsibleHeadings`问题》在Ju... 目录jupyterLab打开后出现“templandroidate_path”相关问题这是 tensorflo

如何解决Pycharm编辑内容时有光标的问题

《如何解决Pycharm编辑内容时有光标的问题》文章介绍了如何在PyCharm中配置VimEmulator插件,包括检查插件是否已安装、下载插件以及安装IdeaVim插件的步骤... 目录Pycharm编辑内容时有光标1.如果Vim Emulator前面有对勾2.www.chinasem.cn如果tools工

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Java多线程父线程向子线程传值问题及解决

《Java多线程父线程向子线程传值问题及解决》文章总结了5种解决父子之间数据传递困扰的解决方案,包括ThreadLocal+TaskDecorator、UserUtils、CustomTaskDeco... 目录1 背景2 ThreadLocal+TaskDecorator3 RequestContextH

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

关于最长递增子序列问题概述

《关于最长递增子序列问题概述》本文详细介绍了最长递增子序列问题的定义及两种优化解法:贪心+二分查找和动态规划+状态压缩,贪心+二分查找时间复杂度为O(nlogn),通过维护一个有序的“尾巴”数组来高效... 一、最长递增子序列问题概述1. 问题定义给定一个整数序列,例如 nums = [10, 9, 2

Spring AI Alibaba接入大模型时的依赖问题小结

《SpringAIAlibaba接入大模型时的依赖问题小结》文章介绍了如何在pom.xml文件中配置SpringAIAlibaba依赖,并提供了一个示例pom.xml文件,同时,建议将Maven仓... 目录(一)pom.XML文件:(二)application.yml配置文件(一)pom.xml文件:首