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隐藏掉了。