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

news2024/10/5 19:11:59

在这里插入图片描述

问题描述

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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1798970.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

硬件产品经理

边端协调管理平台 主页一&#xff1a;模型管理1.1 边侧模型管理 二&#xff1a;配置管理2.1 终端软件配置管理 三&#xff1a;设备管理3.1 区域位置管理3.2 工控机管理&#xff08;其实就是围绕授权&#xff09;3.3 生产设备管理3.4 设备运行管理 四&#xff1a;数据服务4.1 实…

算法:94. 二叉树的中序遍历

给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xff1a;root [1] 输出&am…

智慧商砼搅拌车安监运营管理的创新实践

随着城市化进程的加速&#xff0c;商砼搅拌车作为城市建设的重要设备&#xff0c;其安全管理与运营效率直接关系到工程质量和施工进度。近年来&#xff0c;通过引入先进的4G无线视频智能车载终端套件&#xff0c;我们实现了对商砼搅拌车的高精度定位、实时音视频调度、实时油量…

使用 TinyEngine 低代码引擎实现三方物料集成

本文由体验技术团队 TinyEngine 项目成员炽凌创作&#xff0c;欢迎大家实操体验&#xff0c;本体验内容基于 TinyEngine 低代码引擎提供的环境&#xff0c;介绍了如何通过 TinyEngine 低代码引擎实现三方物料集成&#xff0c;帮助开发者快速开发。 知识背景 1.1 TinyEngine 低…

用互斥锁解决缓存击穿

我先说一下正常的业务流程&#xff1a;需要查询店铺数据&#xff0c;我们会先从redis中查询&#xff0c;判断是否能命中&#xff0c;若命中说明redis中有需要的数据就直接返回&#xff1b;没有命中就需要去mysql数据库查询&#xff0c;在数据库中查到了就返回数据并把该数据存入…

燃烧截稿倒计时,NDSS‘25大会即将召开,你的论文准备好了吗?

燃烧截稿倒计时&#xff01;NDSS25大会即将召开&#xff0c;你的论文准备好了吗&#xff1f; 第32届NDSS25(Network and Distributed System Security Symposium)即网络与分布式系统安全研讨会将于2025年2月23日至28日在加利福尼亚州圣地亚哥举行&#xff01; 作为信息安全领域…

WWDC24即将到来,ios18放大招

苹果公司即将在下周开全球开发者大会(WWDC)&#xff0c;大会上将展示其人工智能技术整合到设备和软件中的重大进展,包括与OpenAI的历史性合作。随着大会的临近,有关iOS 18及其据称采用AI技术支持的应用程序和功能的各种泄露信息已经浮出水面。 据报道,苹果将利用其自主研发的大…

MySQL的组成与三种log

MySQL由几块组成 连接器分析器优化器执行器 MySQL的三大log blog 作用&#xff1a; 用于主从同步与数据恢复 记录内容&#xff1a; 已经完成的 DML(数据操作语句)&#xff0c;主要是用于数据备份 redolog<重试日志> 作用&#xff1a; 崩溃恢复&#xff0c;用于事…

Linux安装Nacos教程【带图文命令巨详细】

巨详细Linux安装Nacos教程 1、检查是否有残留nacos版本2、上传安装包至服务器2.1安装包获取2.2创建相关目录 3、安装Nacos4、配置Nacos4.1修改数据源4.2新建nacos数据库4.3启动nacos4.4把nacos进程交给systemctl管理4.5设置nacos开机自启动 1、检查是否有残留nacos版本 rpm -q…

随便用css换个渐变的太阳

来源于GPT4o&#xff1a;代码来源 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>渐变色上半圆…

此表单不安全,因此系统已关闭自动填充功能

问题截图&#xff1a; 截图就不放了&#xff0c;公司的系统不方便&#xff0c;就是form表单会有个提示“此表单不安全&#xff0c;因此系统已关闭自动填充功能” 解决思路&#xff1a; 1、问题原因 使用https访问&#xff0c;但表单提交地址是http的 2、查看表单配置 表单…

具有 MOSFET 的电压到电流 (V-I) 转换器电路

设计说明 该单电源、低侧、V-I 转换器向可以连接到比运算放大器电源电压更高的电压的负载提供经过良好调节的电流。该 电路接受介于 0V 和 2V 之间的输入电压&#xff0c;将其转换为介于 0mA 和 100mA 之间的电流。通过将低侧电流检测电 阻 R3 上的压降反馈到运算放大器的反相…

C语言详解(动态内存管理)1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

《广告数据定量分析》读书笔记之统计原理2

3.相关分析&#xff1a;描述的是两个数值变量间关系的强度。&#xff08;两个数值型变量之间的关系&#xff09; &#xff08;1&#xff09;图表表示&#xff1a;散点图 &#xff08;2&#xff09;衡量关系强度指标&#xff1a;相关系数r。 &#xff08;r的取值为-1到 1&…

图的创建和BFS,DFS遍历

图的创建 图是一种用于表示对象及其关系的抽象数据结构&#xff0c;由节点&#xff08;也称为顶点&#xff09;和连接节点的边组成。图可以分为有向图&#xff08;Directed Graph&#xff09;和无向图&#xff08;Undirected Graph&#xff09;&#xff0c;以及加权图&…

Chrome 源码阅读:跟踪一个鼠标事件的流程

我们通过在关键节点打断点的方式&#xff0c;去分析一个鼠标事件的流程。 我们知道chromium是多进程模型&#xff0c;那么&#xff0c;我们可以推测&#xff1a;一个鼠标消息先从主进程产生&#xff0c;再通过跨进程通信发送给渲染进程&#xff0c;渲染进程再发送给WebFrame&a…

elementUI - 折叠以及多选的组件

//子组件 <template><!-- 左侧第二个 --><div class"left-second-more"><div class"layer-list-wrapper1"><el-collapse v-model"activeNames" change"handleChange"><el-collapse-item v-for"…

企业必备技能-打造全屏轮播图的终极指南

标题&#xff1a;“视觉盛宴&#xff1a;打造全屏轮播图的终极指南” 引言 在网页设计中&#xff0c;轮播图是一种常见的视觉元素&#xff0c;它能够吸引访客的注意力并展示重要内容。本文档将指导你如何使用HTML和CSS快速创建一个全屏轮播图&#xff0c;使您的网站更加生动和…

react-学习基础偏

1.新建文件夹 2.vscode引入这个文件夹 3.打开vscode终端 执行命令 npx create-react-app react-basic 创建基本项目&#xff08;react-basic项目文件夹名&#xff09; 4.进入到这个文件夹 可用的一些命令 这就算启动成功 5. 这是项目的核心包 渲染流程

免费实现网站HTTPS访问

HTTPS&#xff08;Hypertext Transfer Protocol Secure&#xff09;是一种基于SSL协议的HTTP安全协议&#xff0c;旨在为客户端&#xff08;浏览器&#xff09;与服务器之间的通信提供加密通道&#xff0c;确保数据在传输过程中的保密性、完整性和身份验证。与传统的HTTP相比&a…