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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python基础实例

下一个更大的数 定义一个Solution类&#xff0c;用于实现next_great方法 class Solution: def next_great(self, nums1, nums2): # 初始化一个空字典answer&#xff0c;用于存储答案 answer {} # 初始化一个空列表stack&#xff0c;用于存储待比较的数字 stack [] # 遍历nu…

RK3568技术笔记之三 SAIL-RK3568开发板板卡功能测试

从这里开始&#xff0c;就是老生常谈系列之一&#xff1a;板卡功能测试。 放一张图镇一下帖 按照我自己顺手的方式&#xff0c;把这板子功能测一下。 先把开发板串口信息打印出来。 工具 功能 备注 电脑&#xff08;必备&#xff09; 提供使用终端软件环境 需要具备至少…

《精通ChatGPT:从入门到大师的Prompt指南》第1章:认识ChatGPT

第1章&#xff1a;认识ChatGPT 1.1 ChatGPT是什么 ChatGPT&#xff0c;全称为Chat Generative Pre-trained Transformer&#xff0c;是由OpenAI开发的一种先进的自然语言处理模型。它利用了深度学习中的一种技术——Transformer架构&#xff0c;来生成类人文本。ChatGPT通过对…

计算机组成原理(一)

冯诺依曼机器的特征&#xff1a; 指令和数据以同等的地位存储在存储器当中指令和数据都是二进制指令和数据都是保存在存储器当中的 存储字 每个存储单元中的数据&#xff0c;称为存储字 存储字长 存储单元能够存储的二进制数据的长度 在一个8位系统中&#xff0c;字长是…

中学生学人工智能系列:如何用AI学政治

经常有读者朋友给公众号《人工智能怎么学》留言咨询如何使用人工智能学习语文、数学、英语等科目。这些都是中学教师、中学生朋友及其家长们普遍关注的问题。仅仅使用留言回复的方式&#xff0c;不可能对这些问题做出具体和透彻的解答&#xff0c;因此本公众号近期将推出中学生…

最大矩形问题

柱状图中最大的矩形 题目 分析 矩形的面积等于宽乘以高&#xff0c;因此只要能确定每个矩形的宽和高&#xff0c;就能计算它的面积。如果直方图中一个矩形从下标为 i 的柱子开始&#xff0c;到下标为 j 的柱子结束&#xff0c;那么这两根柱子之间的矩形&#xff08;含两端的柱…

EE trade:通货膨胀时期投资什么最好

当通货膨胀来袭&#xff0c;货币购买力下降&#xff0c;闲置资金贬值速度加快。为了有效抵御通货膨胀&#xff0c;投资者需要选择能够保值甚至增值的投资工具。以下是几种在通货膨胀环境下较为理想的投资选择&#xff1a; 1. 投资股票 收益性和风险性&#xff1a;股票虽然风险…

访问成员变量(反射)

文章目录 前言一、访问成员变量的方法二、Field类 1.常用方法2.实操展示总结 前言 为了实现随时随地调用某个类的某个成员变量&#xff0c;我们可以通过反射的Field类进行调用。这其中需要我们获取该类的Class对象&#xff0c;再调用Field类的相关方法&#xff0c;实时地灵活地…

时间序列新范式!多尺度+时间序列,刷爆多项SOTA

当我们面对复杂模式和多变周期的应用场景&#xff08;比如金融市场分析&#xff09;时&#xff0c;采用多尺度时间序列来做分析和预测是个更好的选择。 这是因为&#xff1a;传统时序方法通常只用固定时间窗口来提取信息&#xff0c;难以适应不同时间尺度上的模式变化。但多尺…

Ezsql(buuctf加固题)

开启环境 SSH连接 第一个为页面地址WEB服务 or 11# 利用万能密码登录 密码可以随便输入或者不输入 这里就可以判断这个题目是让我们加固这个登录页面 防止sql注入 查看index.php 添加以下代码 $username addslashes($username); $password addslashes($password);…

【C++】STL中List的基本功能的模拟实现

前言&#xff1a;在前面学习了STL中list的使用方法&#xff0c;现在我们就进一步的讲解List的一些基本功能的模拟实现&#xff0c;这一讲博主认为是最近比较难的一个地方&#xff0c;各位一起加油。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; …

Unity DOTS技术(九) BufferElement动态缓冲区组件

文章目录 一.简介二.例子 一.简介 在之前的学习中我们发现Entity不能挂载相同的组件的. 当我们需要用相同的组件时则可以使用.IBufferElementData接口 动态缓冲区组件来实现 二.例子 1.创建IBufferElementData组件 using Unity.Entities; using UnityEngine; //[GenerateAu…

C语言基础——函数

ʕ • ᴥ • ʔ づ♡ど &#x1f389; 欢迎点赞支持&#x1f389; 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言基础&#xff1b; 文章目录 前言 一、函数的概念 二、库函数 2.1 库函数和头文件 2.2 库函数的使用/…

10.【机器学习】十大算法之一决策树(Decision tree)算法原理讲解

【机器学习】十大算法之一决策树&#xff08;Decision tree&#xff09;算法原理讲解 一摘要二个人简介三什么是决策树四什么是树4.1 二叉树4.1.1 特殊的二叉树&#xff1a;4.1.2 举例说明 五决策树的优缺点5.1 优点5.2 缺点 六算法原理七信息熵八信息增益九信息增益率十基尼指…

2 - 寻找用户推荐人(高频 SQL 50 题基础版)

2.寻找用户推荐人 考点: sql里面的不等于&#xff0c;不包含null -- null 用数字判断筛选不出来 select name from Customer where referee_id !2 OR referee_id IS NULL;

uniapp中使用百度ocr识别引入项目

uniapp中使用百度ocr识别引入项目 官网申请地址 orcAPI文档地址 1.先获取token const getToken () > {uni.request({url: https://aip.baidubce.com/oauth/2.0/token,method: POST,data: {grant_type: client_credentials,client_id: **, apikeyclient_secret: **, skey…

实现开源可商用的 ChatPDF RAG:密集向量检索(R)+上下文学习(AG)

实现 ChatPDF & RAG&#xff1a;密集向量检索&#xff08;R&#xff09;上下文学习&#xff08;AG&#xff09; RAG 是啥&#xff1f;实现 ChatPDF怎么优化 RAG&#xff1f; RAG 是啥&#xff1f; RAG 是检索增强生成的缩写&#xff0c;是一种结合了信息检索技术与语言生成…

刷代码随想录有感(94):划分字母区间(怪!)

题干&#xff1a; 代码&#xff1a; class Solution { public:vector<int> partitionLabels(string s) {int hash[26] {0};for(int i 0; i < s.size(); i) hash[s[i] - a] i;vector<int> res;int left 0;int right 0;for(int i 0; i < s.size(); i){r…

算法2:滑动窗口(下)

文章目录 水果成篮找到字符串中所有字母异位词串联所有单词的子串*最小覆盖子串* 水果成篮 两元素排空操作 窗口中存在元素交错情况&#xff0c;所以出窗口一定要出干净&#xff01;&#xff01;&#xff01; class Solution { public:int totalFruit(vector<int>& …

AI图书推荐:《如何利用ChatGPT在线赚钱》

这本书《如何利用ChatGPT在线赚钱》&#xff08;$100m ChatGPT_ How To Make Money Online With ChatGPT -- Sharp, Biily -- 2023 &#xff09;主要阐述如何利用ChatGPT这一强大的语言模型工具在互联网上创造收入。 以下是各章节内容的概要&#xff1a; **引言** - 介绍了Chat…