【问题分析】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隐藏掉了。

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值