【问题分析】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。

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

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

相关文章

小学一年级数学上册,我终于学完了

目录 一、背景二、过程1.我对课程中的一些知识的思考2.我对于产品的思考3.我对自己儿子与知识产品结合的思考4.产品反馈的那些有意思的数据 三、总结 一、背景 简约而不简单&#xff0c;即是曾经的再现&#xff0c;也是未来的延伸&#xff0c;未来已来&#xff0c;就在脚下。 …

公司面试题总结(一)

1.说说你对盒子模型的理解&#xff0c;如何切换 当对一个文档进行布局的时候&#xff0c;浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型&#xff0c; 将所有元素表示为一个个矩形的盒子 • content&#xff0c;即实际内容&#xff0c;显示文本和图像 • boreder&am…

export 和 export default 的区别

在 JavaScript 中&#xff0c;export 和 export default 都是用于导出模块中的内容的关键字&#xff0c;但它们有一些区别&#xff1a; export: export 关键字用于导出多个变量、函数或对象。可以一次导出多个内容&#xff0c;并且在导入时需要使用对应的名称。例如&#xff1a…

qmt量化交易策略小白学习笔记第18期【qmt编程之获取对应周期的北向南向数据--方式2:原生python】

qmt编程之获取对应周期的北向南向数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取对应周期的北向南向数据 提示 该数据通过get_market_data_ex接口获取获取历史数据前需要先用downl…

增加强制索引依然慢

版本: 阿里云RDS MySQL 8.0.25 线上数据库CPU达到100%, 定位到如下SQL EXPLAIN SELECT ssd.goods_no,ssd.goods_name,ssd.goods_spec,ssd.goods_unit,ssd.create_time,w.warehouse_name,sb.batch_no,swl.warehouse_region_location_name,sc.customer_name AS goodsOwnerName,s…

idm2024最新完美破解版免费下载 idm绿色直装版注册机免费分享 idm永久激活码工具

IDM 2024破解版重新开发了调度程序和MMS协议支持、重新设计和增强的下载引擎、与所有最新浏览器的独特高级集成、改进的工具栏以及大量其他改进和新功能&#xff0c;这一全新的更新&#xff0c;使得IDM下载器更加完美。值得一提的是&#xff0c;它可以借助油猴浏览器的脚本&…

Maven核心功能依赖和构建管理

1.依赖管理和配置 Maven 依赖管理是 Maven 软件中最重要的功能之一。Maven 的依赖管理能够帮助开发人员自动解决软件包依赖问题&#xff0c;使得开发人员能够轻松地将其他开发人员开发的模块或第三方框架集成到自己的应用程序或模块中&#xff0c;避免出现版本冲突和依赖缺失等…

苹果手机618大降价重登销量榜首 红米K70pro为何成京东618国产手机之光

今天的618已经好几天了&#xff0c;很多买有机的已经下单&#xff0c;不出意外苹果15系列手机仍然是最卖座的手机&#xff0c;大家虽然口号喊得很响身体却是诚实的。但令人感到意外的是&#xff0c;今年618国产手机的第一把交椅确实红米K70系列&#xff0c;说好的支持华为呢&am…

45-4 护网溯源 - 溯源相关思路

一、先了解国家的相关法规 根据中华人民共和国网络安全法和刑法相关规定,严禁从事危害网络安全的活动,包括但不限于入侵他人网络、扰乱网络正常功能、窃取网络数据等行为。任何个人和组织都不得以非法方式获取公民个人信息、出售或非法提供个人信息给他人。违反法律规定,给…

如何在手机上恢复误删除的视频?

说到移动设备上的视频恢复&#xff0c;我们仍将揭开4种解决方案供您使用。希望它们对您的案件有所帮助。 众所周知&#xff0c;我们移动设备上的视频应用程序将创建一个缓存文件夹&#xff0c;以在它们永远消失之前临时存储已删除的项目。因此&#xff0c;有许多iPhone / Andr…

C++基础与深度解析 | 模板 | 函数模板 | 类模板与成员函数模板 | concepts | 完美转发 | 模板的其他内容

文章目录 一、函数模板二、类模板与成员函数模板三、Concepts(C20)四、模板相关内容1.数值模板参数与模板模板参数2.别名模板与变长模板3.包展开与折叠表达式4.完美转发与lambda表达式模板5.消除歧义与变量模板 一、函数模板 在C中&#xff0c;函数模板是一种允许你编写可以处理…

iOS 实现蓝牙设备重连的四种方式

文章目录 一、通过identifiers的方式实现重连二、通过UUID的方式实现重连三、通过scan的方式实现重连四、通过didDisconnect后回连实现重连 一、通过identifiers的方式实现重连 /*!* method retrievePeripheralsWithIdentifiers:** param identifiers A list of <code…

java项目使用jsch下载ftp文件

pom <dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.55</version> </dependency>demo1&#xff1a;main方法直接下载 package com.example.controller;import com.jcraft.jsch.*; im…

【mysql】数据报错: incorrect datetime value ‘0000-00-00 00:00:00‘ for column

一、问题原因 时间字段在导入值0000-00-00 00:00:00或者添加 NOT NULL的时间字段时&#xff0c;会往mysql添加0值&#xff0c;此时可能出现此报错。 这是因为当前的MySQL不支持datetime为0&#xff0c;在MySQL5.7版本以上&#xff0c;默认设置sql_mode模式包含NO_ZERO_DATE, N…

基于UrBAN数据集:用声音监测和预测蜜蜂群体的健康状况

蜜蜂在生态平衡中扮演着关键角色&#xff0c;是农业作物和自然生态系统中的重要传粉者。它们不仅生产蜂蜜和蜂蜡&#xff0c;还对许多水果和种子作物产生影响&#xff0c;包括杏仁、柑橘类水果和蓝莓等。蜜蜂群体的健康状况和数量的下降可能对农业产业产生重大影响。全球范围内…

Python爬取城市空气质量数据

Python爬取城市空气质量数据 一、思路分析1、寻找数据接口2、发送请求3、解析数据4、保存数据二、完整代码一、思路分析 目标数据所在的网站是天气后报网站,网址为:www.tianqihoubao.com,需要采集武汉市近十年每天的空气质量数据。先看一下爬取后的数据情况: 1、寻找数据…

2024全国高考作文题解读(文心一言 4.0版本)

新课标I卷 阅读下面的材料&#xff0c;根据要求写作。&#xff08;60分&#xff09; 随着互联网的普及、人工智能的应用&#xff0c;越来越多的问题能很快得到答案。那么&#xff0c;我们的问题是否会越来越少&#xff1f; 以上材料引发了你怎样的联想和思考&#xff1f;请写…

大模型基础——从零实现一个Transformer(1)

一、Transformer模型架构图 主要模块&#xff1a; embedding层&#xff1a; Input/Output Embedding&#xff1a; 将每个标记(token)转换为对应的向量表示。 Positional Encoding&#xff1a;由于没有时序信息&#xff0c;需要额外加入位置编码。 N个 block堆叠: Multi-Head …

【QT5】<总览四> QT常见绘图、图表及动画

文章目录 前言 一、QFile类读写文件 二、QPainter绘简单图形 三、QChart图表 四、QPropertyAnimation属性动画 五、Q_PROPERTY宏简介 六、自定义属性动画 前言 承接【QT5】&#xff1c;总览三&#xff1e; QT常用控件。若存在版权问题&#xff0c;请联系作者删除&#…

Debezium系列之:记录一次debezium采集tdsql-c数据库数据丢失原因的排查

Debezium系列之:记录一次debezium采集tdsql-c数据库数据丢失原因的排查 一、背景二、排查数据丢失时间段数据采集情况三、捕获提交异常信息四、定位原因五、解决方案一、背景 debezium采集tdsql-c数据库,偶尔会出现数据丢失的情况,出现多次后决定整个链路排查定位问题二、排…