【问题分析】TaskDisplayArea被隐藏导致的黑屏以及无焦点窗口问题【Android 14】

在这里插入图片描述

1 问题描述

用户操作出的偶现的黑屏以及无焦点窗口问题。

直接原因是,TaskDisplayArea被添加了eLayerHidden标志位,导致所有App的窗口不可见,从而出现黑屏和无焦点窗口问题,相关log为:

在这里插入图片描述

这个log是MTK添加的,用来分析ANR问题还是非常有帮助的,对于分析黑屏问题同样有用。

2 log分析

该问题复现步骤如下:

1)、在google的dialer app中拨打一个电话,启动”com.google.android.dialer/com.android.dialer.incall.activity.ui.InCallActivity“界面。

2)、按下一个特殊按键,按下该按键后会切换HomeApp,从”com.tcl.android.launcher“切换到”com.tct.book“,因此会新启动一个Activity,”com.tct.book/.ui.MainActivity“。此阶段还会因为设置了新壁纸,导致CONFIG_ASSETS_PATHS改变触发全局Configuration的更新。

3)、接着马上再连续Power键灭屏、亮屏,此时就有可能复现黑屏的情况。

其中第二步的按键操作是特殊定制的。

接下来分析log:

在这里插入图片描述

3 原因分析

这个问题其实之前遇到过一次,只不过当时是解决monkey跑出的ANR问题的,从log上看到也是于TaskDisplayArea被设置了eLayerHidden标志位,导致了所有的App窗口都被视为是不可见的,从而无法获取焦点,出现了ANR。这个问题当时并没有找到原因,结果这次测试直接手动复现出来了。

3.1 什么情况下会为TaskDisplayArea设置eLayerHidden标志位

先看一下当时关于什么情况下会为TaskDisplayArea设置eLayerHidden标志位的分析。

本地尝试后发现,一般情况下直接点击Power键灭屏,是不会为TaskDisplayArea对应的Layer设置eLayerHidden标志位的。

但当灭屏后再去打电话,此时会调起InCallUI。接着取消通话,InCallUI移除,此时会为TaskDisplayArea添加eLayerHidden标志位:

在这里插入图片描述

然后断点发现添加改标志位的代码为:

在这里插入图片描述

当解锁后,会为TaskDisplayArea去掉该标志位,断点后发现在:

在这里插入图片描述

都是在Transitions.setupStartState中:

在这里插入图片描述

逻辑还是比较简单的:

1)、如果该WindowContainer对应的Transition.ChangeInfo/TransitionInfo.Change的动画类型是TRANSIT_OPEN或者TRANSIT_TO_FRONT,那么就在动画开始执行前为其调用Transition.show。

2)、如果该WindowContainer对应的Transition.ChangeInfo/TransitionInfo.Change的动画类型是TRANSIT_CLOSE或者TRANSIT_TO_BACK,那么就在动画开始执行前为其调用Transition.hide。

我们从log能看到TaskDisplayArea是参与了动画的,并且它的ChangeInfo的类型就是TRANSIT_TO_BACK,所以在Transitions.setupStartState中就会为TaskDisplayArea的SurfaceControl调用Transition.hide为其Layer添加eLayerHidden标志位。

3.2 问题的直接原因

接着再回到我们现在的这个问题。

先看最直接的那个原因:

在这里插入图片描述

疑点有两个:

1)、为什么Task提升为了TaskDisplayArea?

2)、为什么TaskDisplayArea动画类型为TO_BACK?

梳理一下这个Transition#76的上下文,标注几个关键节点:

1)、”com.tct.book/com.tct.book.ui.MainActivity“启动并且绘制完成。

2)、设置新壁纸,全局Configuration发生改变,此时创建了Transition#76,类型为TRANSIT_CHANGE。

3)、Dialer又重新启动了”com.google.android.dialer/com.android.dialer.incall.activity.ui.InCallActivity“,新建了Task#24。

4)、按下Power键创建了TRANSIT_SLEEP类型的Transition#77,不过由于Transition#76正在收集,所以Transition#77进行了排队,并且刚刚启动的InCallActivity也因为”sleep“的原因被pause。

5)、Transition#76走到Transition.onTransactionReady。

6)、Transition#77开始收集。

7)、InCallActivity重新变为resume。

8)、Transition#77被abort。

接下来开始分析。

3.3 对Task提升为TaskDisplayArea的分析

从”Initial targets:“这条log我们可以看到,最初TaskDisplayArea是没有直接被收集到Transition中的,而是从经过了两次PROMOTE之后,被收集了进来:

1)、检查”com.tct.book“的Task#23,发现可以提升到Launcher的RootTask,Task#1(这里的”com.tct.book“也是一个Launcher App)。

2)、又检查Launcher的RootTask,Task#1,发现可以提升到TaskDisplayArea。

3)、TaskDisplayArea由于灭屏的原因,其mVisibleRequested被置为false,导致Transition.ChangeInfo.getTransitMode方法为其选择了TRANSIT_TO_BACK的动画。

”com.tct.book“的Task#23,首先是因为全局Configuration改变的原因,被添加到了Transition#76中:

在这里插入图片描述

因此首先肯定是会为该Task创建ChangeInfo对象,并且加入到Transition.mChanges中。然后根据我们对这段代码的理解,一般这个时候,也会为TaskDisplayArea以及DisplayContent创建ChangeInfo对象并且加入到Transition.mChanges中。这就为后续Transition.onTransactionReady的时候,将Task提升到TaskDisplayArea提供了可能。

Transition#76走到Transition.onTransactionReady的时候,检查Task#23是否可以提升的时候,看到它的所有姊妹Task都没有参与动画,并且都是不可见的,因此就认为可以提升,从而动画的主体就从Task#23变成为了TaskDisplayArea。

根据我们的上下文分析,Transition#76走到Transition.onTransactionReady之前,正好按下了Power键,并且之前resume的inCallActivity也的确因为”sleep“的原因变成pause了,那么说明Dialer对应的Task#24是不可见的,因此Task#23就可以提升为TaskDisplayArea。

3.4 对TaskDisplayArea动画类型为TO_BACK的分析

首先从生成的TransitionInfo的信息看到TaskDisplayArea的动画类型为TO_BACK,动画类型在Transition.ChangeInfo.getTransitMode中计算:

在这里插入图片描述

由于整个过程中:

1)、没有transientLaunch相关的启动。

2)、TaskDisplayArea始终是存在的,因此mExistenceChanged是不会有变化的。

因此只会根据其可见性返回TRANSIT_TO_FRONT或者TRANSIT_TO_BACK,并且我们从后续的log信息知道了这里返回了TRANSIT_TO_BACK,说明此时TaskDisplayArea调用isVisibleRequested返回了false:

在这里插入图片描述

成员变量mVisibleRequested只在WindowContainer.setVisibleRequested方法中进行设置:

在这里插入图片描述

查看该方法调用的地方:

在这里插入图片描述

只有一处地方可能和TaskDisplayArea相关,即WindowContainer.onChildVisibleRequestedChanged:

在这里插入图片描述

该方法在ActivityRecord调用setVisibleRequested方法设置ActivityRecord的时候就会调用,用来反作用于Task以及更高级别的WindowContainer的可见性。

大致看下该方法,发现逻辑还是比较好理解的:

1)、如果当前WindowContainer是不可见的,但是传入的这个子WindowContainer被设置为了可见,那么就设置当前WindowContainer为可见。

2)、如果当前WindowContainer是可见的,但是传入的这个子WindowContainer被设置为了步可见,那么继续寻找其它子WindowContainer中是否有可见的,只要有一个子WindowContainer是可见的,那么当前WindowContainer仍然应该被认为是可见的。只有所有子WindowContaienr都不可见了,那么当前WindowContainer才会被认为是不可见的。

回到我们的问题中,很显然单纯的App切换并不能导致TaskDisplayArea变成不可见,再回顾我们发生问题时的操作步骤,似乎也只有灭屏能做到了。

灭屏,所有Activity都会被pause、stop,变为不可见 -> 所有Task都不可见 -> TaskDisplayArea不可见。后续打了log后发现的确如此,Task或者TaskDisplayArea都不能主动设置自身的可见性,只能是ActivityRecord先主动设置ActivityRecord的可见性,然后再影响他们的可见性。

3.5 InCallActivity重新resume的时候没有恢复吗

在这里插入图片描述

看到log,虽然后续InCallActivity重新又被设置为了resumeActivity,但是此时这里的新建的Transition#77被abort了,并且这也是最后一个Transition了,导致后续没有办法重新为TaskDisplayArea调用Transtion.show方法,所以后续无法恢复。

如果Transition#77没有被abort,并且基于这里的信息只有Dialer参与了动画,那么Dialer是可见的,并且Launcher没有参与动画并且不可见,所以Dialer对应的Task是有机会提升为TaskDisplayArea的,那么是有机会恢复的。

那么再看下这个Transition#77的情况:

在这里插入图片描述

首先这个Transition#77是一个SLEEP类型的Transition,它在按下Power键准备灭屏的时候创建,此时Transition#76正在收集,所以它被推迟,进行了排队。

在这里插入图片描述

Transition#77开始收集,是在Transition#76走到Transition.onTransactionReady的时候,此时看到正好InCallActivity被设置为resume了,那么它应该也被设置为可见了,但是那么Transition#77就被abort了。

又回到这个问题了,为什么Transition#77被abort了呢?

Transition#77对应的是按下Power键灭屏的流程,它的类型是SLEEP,因此我们可以知道它应该是在RootWindowContainer.applySleepTokens中创建的:

在这里插入图片描述

涉及到我们的分析的内容为,遍历所有DisplayContent:

1)、创建一个TRANSIT_SLEEP类型的Transition对象。

2)、创建一个TransitionController.OnStartCollect类型的接口类,包含一个名为onCollectStarted的回调方法。

3)、判断当前是否有Transition正在收集,如果没有,那么直接将第一步创建的Transition对象移动到收集状态,否则调用TransitionController.startCollectOrQueue方法。

从log中我们知道了此时是将这个TRANSIT_SLEEP类型的Transition拿去排队了,即调用了TransitionController.startCollectOrQueue:

在这里插入图片描述

这里的逻辑也比较简单,如果当前有Transition正在收集,那么再检查一下刚刚创建的这个Transition能否和这个正在收集的Transition并行收集,如果不行,那么调用Transition.queueTransition将这个新创建的Transition添加到等待队列中,即成员变量mQeuedTransitions中。

需要注意的是mQeuedTransitions是一个QueuedTransition的队列,QueuedTransition是对Transition还有TransitionController.OnStartCollect做的一层封装。

后续正在排队的Transition会在TransitionController.tryStartCollectFromQueue中被取出:

在这里插入图片描述

内容大致是:

1)、从mQeuedTransitions中取出队首的那个Transition,为其调用TransitionController.moveToCollecting移动到收集状态。
2)、调用之前排队时传入的那个TransitionController.OnStartCollect接口类的onCollectStarted回调。

这个TransitionController.OnStartCollect对象我们之前是在RootWindowContainer.applySleepTokens方法中创建的:

在这里插入图片描述

如果这个回调执行的时候被推迟,并且此时屏幕不应该被休眠,那么将这个Transition中止掉,这个Transition自然就是上面创建的TRANSIT_SLEEP类型的Transition了。再回到我们问题的场景,很显然这个TRANSIT_SLEEP的Transition就是Transition#77,他之前是被推迟了,并且走到这里的时候,InCallActivity已经因为”sleep“被pause后重新又resume了,所以说明此时屏幕已经唤醒了,也就说屏幕不应该休眠,所以这个Transition就被abort了。

4 复现问题

再次根据log总结一下复现问题的几个关键点,总结出该问题复现的一般路径:

1)、写一个Activity1,按下按钮设置壁纸,设置壁纸后 —— 发生ConfigChange,创建Transition1,类型为TRANSIT_CHANGE。

2)、以new task的方式新启动一个Activity2。

2)、按Power键灭屏 —— 创建Transition2,类型为TRANSIT_SLEEP,并且被延迟,排队等候,并且Activity2被pause,TaskDisplayArea被设置为不可见。

3)、Transition1走到Transition.onTransactionReady,后续会为TaskDisplayArea添加eLayerHidden标志位。

4)、按下Power键亮屏,Activity2重新resume,并且Transition2被abort。

大概的代码为在Activity.onCreate里初始化一个Button,按下按钮后调用setWallpaper方法设置壁纸,并且在短暂的延迟后以NEW_TASK的方式启动另外一个Activity:

        changeWallpaper = findViewById(R.id.changeWallpaper);changeWallpaper.setOnClickListener((v) -> {setWallpaper();Handler handler = new Handler();handler.postDelayed(() -> {startActivity(new Intent(MainActivity.this, LongDrawActivity.class));}, 100);});

setWallpaper的大致为:

    private void setWallpaper() {WallpaperManager wallpaperManager = WallpaperManager.getInstance(this);try {InputStream inputStream;if (mWallpaperId == 2) {inputStream = getAssets().open("1.png");mWallpaperId = 1;} else if (wallpaperId == 1) {inputStream = getAssets().open("2.png");mWallpaperId = 2;} else {inputStream = getAssets().open("4.png");mWallpaperId = 2;}Bitmap bitmap = BitmapFactory.decodeStream(inputStream);wallpaperManager.setBitmap(bitmap);} catch (IOException e) {e.printStackTrace();}}

该问题还是非常难复现的,以上顺序不能出错,不然无法复现到问题,并且两次按Power键的时间也非常难掌握,只能进行多次尝试,运气好了可能会复现一次。

5 解决问题

经过多次尝试,最终成功在pixel上复现了…和我们的问题发生时一样的log,但是pixel没问题:

在这里插入图片描述

看到这里TaskDisplayArea也是只参与了一次动画,并且类型为TO_BACK,但是为什么pixel没问题呢?

原来时后面跟了一句很关键的log:

4-26 18:07:25.791 1829 5630 E TransitionController: DisplayArea became visible outside of a transition: DefaultTaskDisplayArea@65482673

正是这里,将TaskDisplayArea重新变成了可见,而我们的代码里没有这个patch。

最后在google网站上找到该patch,把这个patch打上后问题解决:

在这里插入图片描述

最后大概看一下这个TransitionController.validateStates方法,很明显这是一个纠错的机制,该方法在Transition流程的最后TransitionController.finishTransition方法中才调用,防止动画结束后把不该隐藏的WindowContainer隐藏掉了。

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

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

相关文章

Django模型继承之多表继承

在Django模型继承中,支持的第二种模型继承方式是层次结构中的每个模型都是一个单独的模型。每个模型都指向分离的数据表,并且可以被独立查询和创建。在继承关系中,子类和父类之间通过一个自动创建的OneToOneField进行连接。示例代码如下&…

C语言入门课程学习笔记-6

C语言入门课程学习笔记-6 第27课 - 字符数组与字符串(上)第28课 - 字符数组与字符串(下)第29课 - 数组专题练习(上)第30课 - 数组专题练习(下) 本文学习自狄泰软件学院 唐佐林老师的…

不只有 Spring,这四款Java 基础开发框架同样值得关注!

Java 开发不只有 Spring ,今天给大家推荐几个同样优秀的 Java 基础开发框架,为日常项目开发提供更多的选择。答应我,请不要再叫我 Spring 小子了,​好吗? 项目概览: Guice:轻量级依赖注入框架 …

2024Mac系统热门游戏排行榜 Mac支持的网络游戏有哪些?mac能玩哪些大型网游 苹果电脑Mac游戏资源推荐 Mac玩Windows游戏

“游戏是这个世界上唯一能和女性争夺男朋友的东西(/滑稽,有不少女生也喜欢玩游戏)。” 虽然只是一句玩笑话,不过也可以看出游戏对大多数男生来说是必不可少的一项娱乐活动了。而网络游戏是游戏中的一大分支,能让玩家们…

科技“冷”战:NIST刷新制冷效率,中国实力逆境崛起!

4月23日,美国国家标准与技术研究院(NIST)的研究人员报道称,他们通过对常用于科研和工业领域的制冷机进行改装,显著降低了将材料冷却至略高于绝对零度所需的时间和能量。 科学家们指出,他们的原型设备每年能…

Linux 学习之路 -- 进程篇 -- 进程控制

目录 一、进程终止 <1>使用语言和系统自带的方法&#xff0c;进行转换 <2>自定义错误码 <3>小结&#xff1a; <2>两个接口exit / _exit 二、进程等待 <1>简单了解 <2>wait调用 <3>waitpid调用 <4>status <1>W…

复杂的字符串算法——KMP算法

字符串算法 模式匹配&#xff08;Pattern Matching&#xff09;&#xff1a;在一篇长度为 &#x1d45b; 的文本 &#x1d446; 中&#xff0c;找某个长度为 &#x1d45a; 的关键词 &#x1d443;。&#x1d443; 可能多次出现&#xff0c;都需要找到。 最优的模式匹配算法复…

AHB传输---突发操作

突发操作 在本协议中定义了4拍、8拍和16拍的突发&#xff0c;以及未定义长度的突发和单次传输。它支持增量和包装突发&#xff1a; 增量突发访问连续位置&#xff0c;每个传输的地址是前一个地址的增量。包装突发在跨越地址边界时会包装。地址边界的计算方法是突发中拍数与传…

Android—统一依赖版本管理

依赖版本管理有多种方式 config.gradle 用于Groovy DSL&#xff0c;新建一个 config.gradle 文件&#xff0c;然后将项目中所有依赖写在里面&#xff0c;更新只需修改 config.gradle文件内容&#xff0c;作用于所有module buildSrc 可用于Kotlin DSL或Groovy DSL&#xff0c;…

MATLAB冒号表示法

MATLAB 冒号表示法 colon(:)是在MATLAB中最有用的运算符之一。它用于创建向量&#xff0c;下标数组和指定迭代。 如果要创建包含1到10的整数的行向量&#xff0c;请编写- 示例 1:10 MATLAB执行该语句并返回包含1到10的整数的行向量- ans 1 2 3 4 5 6 7 8 9 10 如果要指定一…

github Copilot的使用总结

1. 代码建议和补全 GitHub Copilot 的基本使用涉及编写代码时的实时代码建议和补全。一旦你已经安装并配置好 GitHub Copilot 插件&#xff0c;你可以在支持的编辑器&#xff08;如 Visual Studio Code&#xff09;中开始使用 Copilot。以下是一些基本的使用步骤&#xff1a; …

VBA技术资料MF146:发出多次Beep提示声

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

K8S 部署和访问 Kubernetes 仪表板(Dashboard)

文章目录 部署 Dashboard UI浏览器访问登陆系统 Dashboard 是基于网页的 Kubernetes 用户界面。 你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中&#xff0c;也可以对容器应用排错&#xff0c;还能管理集群资源。 你可以使用 Dashboard 获取运行在集群中的应用的概览…

unit4.web服务的部署及高级优化方案

搭建web服务器要求如下&#xff1a; 1.web服务器的主机ip&#xff1a;172.25.254.100 [rootserver101 桌面]# vmset.sh 100 连接已成功激活&#xff08;D-Bus 活动路径&#xff1a;/org/freedesktop/NetworkManager/ActiveConnection/3&#xff09; [rootserver101 桌面]# ifc…

鑫海移民荣耀呈现:EB5投资移民盛宴落幕,卓越项目引领投资新潮

随着春日的暖阳渐渐铺满大地&#xff0c;我们鑫海移民集团在这个充满希望的季节里&#xff0c;举办了一场意义非凡的EB5投资移民专题活动。于2024年4月27日&#xff08;周六&#xff09;下午13:30&#xff0c;在北京渤海润泽威斯汀酒店隆重举行&#xff0c;我们与众多热情的参与…

基于java的商店积分管理系统的设计与实现

功能需求 从功能上可以划分为个人信息管理、商店管理、平台管理、订单管理和数据分析。后台管理系统主要服务于商户和平台管理员&#xff0c;兑换用户是属于商户平台的自有用户&#xff0c;不会被纳入到后台管理系统中来。商户用户可以对自己的积分进行管理&#xff0c;平台管…

echarts下载图片

toolbox: {show: true,//展示工具栏itemSize:20,//icon的大小iconStyle:{borderColor:"#409eff",borderWidth:"3",color:"#fff"},right:"40px",//偏移位置feature: {saveAsImage: {title: "下载图表", //鼠标滑过之后文案na…

用wps自带工具给图片做标注

在wps中&#xff0c;选中wps中的图片&#xff0c;右键选择【编辑】进入图片编辑器&#xff0c;在选项卡面板右侧选择【标注】工具&#xff0c;再选择【添加文本】工具&#xff0c;即可直接在图片上输入文字&#xff0c;标注完成后选择【覆盖原图】就完成标注任务。

【Canvas与艺术】绘制美国星条旗

注意&#xff1a; 该图位置和大小都是按照网上说明精确绘制的。 【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>使…

FA-128晶振用于医疗设备

血糖仪已成为家庭常用的医疗设备,日本爱普生晶振公司生产的2016封装,32MHz贴片晶振可完美应用于医疗器械血糖仪,此款晶振订货型号为X1E000251005900晶振,型号为FA-128,负载电容分8PF,精度10PPM,其尺寸参数为2.0x1.6x0.5mm,符合ROHS标准且无铅,具有封装尺寸超小,高精度,频率范围…