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

news2024/12/23 1:07:56

在这里插入图片描述

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.coloradmin.cn/o/1630559.html

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

相关文章

目标检测——YOLOv8算法解读

作者:Ultralytics公司 代码:https://github.com/ultralytics/ultralytics YOLO系列算法解读: YOLOv1通俗易懂版解读、SSD算法解读、YOLOv2算法解读、YOLOv3算法解读、YOLOv4算法解读、YOLOv5算法解读、YOLOR算法解读、YOLOX算法解读、YOLOv6算…

7. Django 模型与数据库

第7章 模型与数据库 Django对各种数据库提供了很好的支持, 包括PostgreSQL, MySQL, SQLite和Oracle, 而且为这些数据库提供了统一的API方法, 这些API统称为ORM框架. 通过使用Django内置的ORM框架可以实现数据库连接和读写操作. 本章以SQLite数据库为例, 分别讲述Django的模型…

统一威胁情报如何赋能SOC应对复杂威胁?

安全运营中心(SOC)是组织网络安全战略的核心组成部分,扮演着至关重要的角色。其负责实时监控整个IT基础设施,以检测、响应和预防各类网络安全威胁。网络安全威胁日益复杂且多变的数字化时代,攻击平面泛化、基础设施复杂…

10天精通Python爬虫:详细路线速成,开启兼职副业新篇章!

爬虫,即网络爬虫,是一种自动化程序,用于从互联网上抓取数据。在现代信息社会,爬虫技术广泛应用于数据分析、搜索引擎优化、竞品分析等领域。学习爬虫不仅可以提高数据处理能力,还可以为未来的职业发展打下坚实基础。 …

java基础之java容器-Collection,Map

java容器 java容器分类一. Collection1. List①. ArrayList② . LinkedList③ . Vector 2. Queue队列①. LinkedList②. PriorityQueue 3. Set集合①. HashSet②. TreeSet 二. Map1. HashMap2.TreeMap3. Hashtable java容器分类 java容器分为两大类,分别是Collecti…

修复所有 bug 并不能解决所有问题

原文:jeffpsherman - 2024.04.08 在软件领域,如同在制造业,有些问题是由于 bug 或“特殊原因”引发的,而有些则是“常见原因”,这是由于系统设计和实现的性质所导致的。修复 bug 就是移除特殊原因,消除 bu…

项目经理学习PMP对自己工作有多大帮助?

PMP是一种项目管理认证,也是最流行和含金量较高的认证之一。因此,你问它对项目管理有多大帮助,我可以说PMP可以被视为量身定制给项目管理岗人员的一个证书,你能理解它的重要性吗? 随着国家经济建设由基础设施向高端产业…

新闻 | 电子系协同智能中心与昌平区未来高教园及多所高校开展交流,共话智能无人平台建设

2024年4月8日,清华大学电子工程系在北京昌平两岸共盈科技产业园电子系地空协同智能无人平台基地成功举办“美团杯”智能无人机挑战赛,清华大学电子系党委书记沈渊、昌平区未来城管委会校城融合处处长熊玉川、清华大学团委副书记黄峰等出席。此外来自昌平…

CSS详解(一)

1、css工作中使用场景 美化网页(文字样式、背景样式、边框样式、盒子模型、定位、动画、),布局页面(flex布局、响应式布局、媒体查询) 2、CSS 规则 通常由两个主要部分组成选择器和样式声明 2.1选择器 选择器指定了…

Unity打包PC端exe,压缩打包为一个exe文件

目录 一.打包成功 1.打包输出文件 二.压缩输出目录为exe单个文件 1.添加到压缩文件 2.其他设置 1.点击“高级→自压缩选项” 2.修改解压后运行程序 3.设置模式 4.更新 三、生成.exe 一.打包成功 1.打包输出文件 1、一个后缀为 BurstDebugInformation_DoNotShip的文…

unittest断言_assert_使用python自带的断言

既可以使用: 1. self.assertEqual(ex1, ex2) #判断ex1 是否相等ex2 2. self.assertIn(ex1 ,ex2) # ex2是否包含ex1 注意:所谓的包含不能跳字符 3. self.assertTrue(ex) # 判断ex是否为True又可以使用: """目标断言扩展&…

C#实现 IDbConnection / IDbCommand 等相关通用数据接口

目录 关于数据接口 对象执行流程 范例运行环境 设计与实现 引用 GetConnection方法 GetCommand方法 GetParameter方法 小结 关于数据接口 在.net 应用中,与数据库进行连接、访问和执行经常会用到数据接口的相关对象,如下: 1、 Con…

【mysql】mysql命令使用大全,你想要的都在这里

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…

《前端面试题》- React - 如何区分函数组件和类组件

问题 如何区分函数组件和类组件? 答案 可以使用instanceof 或者Component.prototype.isReactComponent。 示例 函数组件 export default function FunctionComonent() {if(FunctionComonent.prototype.isReactComponent){console.log(FunctionComonent是类组件…

前端JS必用工具【js-tool-big-box】,Number数值转换的方法调用学习

这一小节,我们针对前端工具包(npm)js-tool-big-box的使用做一些讲解,主要是针对Number数值型转换的一些方法使用。 目录 前言 1 安装和引入 2 千位逗号分割 3 判断是否大于0 4 判断是否大于0的整数 5 生成指定范围内的随机数…

Allegro画PCB时如何只删除走线的一部分

如何只删除走线的一部分 1、第一步: 2、第二步: 3、第三步,点击相应的走线段就能删除了。 说明:上面的Cline和Line只的是电线和线,您按下删除后,就可以删除这两种东西,但删除的是一整条折线.把这两个取消掉,换成Cline Segs和Ot…

计算机服务器中了helper勒索病毒怎么办,helper勒索病毒解密流程工具

在网络技术飞速发展的今天,越来越多的企业离不开网络,网络可以为企业带来更高的生产效率,提供更多的便捷服务,但网络是一把双刃剑,在为人们提供便利的同时,也为企业的数据安全带来严重威胁。近日&#xff0…

通过 API从 0 到 1 构建 本地 GPTs——1.构建Builder‘s Prompt

目的:帮助小白用户生成结构化 prompt 功能清单 搭建本地 gpts 能力,构建本地企业知识库助手Builder’s Prompt -对话引导构建 prompt 示例,生成助手信息function_call的用法prompt 示例 GPTs 的 Create 能力 用于引导用户构建结构化的 pr…

【数据分析面试】33.计算加权平均值 (Python: enumerate())

题目 假设你在分析薪资数据。经理让你使用最近加权平均值来计算平均薪资,并为提供了过去’n’年的数据。 最近加权应确保最近几年的薪资权重大于时间较久几年的薪资。 编写函数实现下面计算功能:输入previous_salaries是最近n年的薪资列表&#xff0c…

使用Canal实现MySQL主从同步

说明:本文介绍如何使用Canal实现MySQL主从同步的效果,关于Canal入门使用参考:Canal入门使用 启动Canal 首先,设置Canal服务器里,目标节点(即监测的MySQL节点)的配置,启动Canal服务…