用户操作出的 Launcher ANR,场景为在Launcher界面一个Activity启动又快速销毁导致的无焦点窗口问题。
1 log分析1
最近分析ANR问题的时候,遇到多次在Launcher界面一个Activity启动又快速销毁导致无焦点窗口从而引起ANR的现象,并且本次ANR是人为操作出来的,并非Monkey,因此需要继续根据上下文去看看问题场景,以及是否能够复现。
2 log分析2
因为无法让微信App去按照问题发生时候的情况去行为,本地通过写一个DemoApp模仿微信的App的行为,初步稳定复现了该ANR,且在pixel上也能稳定复现,先总结一下要点。
本地通过DemoApp复现问题的时候,发现通过点击App图标的方式启动DemoApp无法复现问题,而通过adb命令去启动DemoApp可以稳定复现,需要看下这里为什么会呈现差异,这个ANR毕竟是用户操作复现的,而非Monkey。
3 分析两种启动方式的不同点
3.1 adb命令启动Demo App
先尝试通过adb命令启动Demo App的方式去复现问题,发现当MainActivity对应的窗口挂掉后,是去更新了一次焦点窗口的,即走到了DisplayContent.updateFocusedWindowLocked,就在窗口挂掉的流程中:
WindowState.DeathRecipient.binderDied
-> WindowState.removeIfPossible
-> WMS.updateFocusedWindowLocked
log如下:
但是这次发现所有窗口都不符合作为焦点窗口的条件,所以此次没有更新成功,并且只更新了这一次,因此后续DisplayContent.mCurrentFocus就始终为空了。
3.2 点击App图标启动Demo App
如果是通过点击App图标的方式去启动Demo App,发现有两次更新窗口焦点的操作。
第一次就是窗口挂掉的流程,这一次和上面分析的一样,也是没有找到合适的窗口。
接着第二次更新的时候,找到了符合条件的窗口,即Launcher,并且将DisplayContent.mCurrentFocus设置为了Launcher:
首先看到这次更新是因为移除SnapshotStartingWindow窗口触发的,而通过adb命令启动Demo App的时候并没有添加SnapshotStartingWindow窗口,因此通过adb命令的方式少了一次更新焦点窗口的操作,所以复现了问题。
另外查看问题log,发现问题发生那次启动微信App的时候,是没有启动SnapshotStartingWindow的;
因此后续我们只要找到方法,使得通过点击App图标启动Demo App的时候,不要启动SnapshotStartingWindow,就可以复现用户操作出的那种ANR了。
4 log分析3
继续往log前面看,看下上一次启动微信App是什么时候,以及发生了什么。
至此,我们已经找到复现该ANR所需要的所有操作,同样Pixel也可以复现:
1、从Launcher启动Demo App,然后点击Home键回到Launcher(这里不是像问题发生的时候那样点击Back键,后面会解释),让Demo App处于后台。
2、让Demo App自己启动自己一次,此时会因为BAL_BLOCK无法启动成功(如果上一步是点击点击Back键回到Launcher,那么这里可能会成功启动且显示BAL_ALLOW_GRACE_PERIOD,如果是这样那么需要将这次启动Demo App的时间距离第一步启动Demo App的时间间隔大于10s,所以为了省事,第一步直接通过点击Home键回到Launcher,而非Back键),但是无所谓,这一步主要是为了让后续从Launcher启动Demo App的时候不会启动SnapshotStartingWindow。
3、在Launcher界面点击App图标启动Demo App,Demo App启动后需要首先finish,接着crash,此时可以复现DisplayContent.mCurrentFocus为null,即无焦点窗口的情况。
4、在无焦点窗口的情况下,输入一个KeyEvent,我们这里模仿用户输入了一个keycode为98的KeyEvent.KEYCODE_BUTTON_C,即可发生ANR:
这里为复现问题的Demo App代码:
public class MainActivity extends Activity {
int countDown = 1;
private Activity test = new Activity();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onStart() {
super.onStart();
if (countDown == 0) {
finish();
test = null;
test.finish();
}
if (countDown == 1) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(MainActivity.this, MainActivity.class);
startActivity(intent);
}
}, 5000);
}
countDown--;
}
}
5 ANR原因分析
最后分析为什么会出现ANR,这里根据我本地复现的log进行分析,从窗口挂掉后,有3个时间点都是有机会更新焦点窗口的。
5.1 关键点1
首先是Demo App挂掉后,移除WindowState的流程:
从log能看到这里其实是去更新了一次焦点窗口的,但是此次由于Launcher对应的ActivityRecord.mVisibleRequested为false,所以不满足作为焦点窗口的条件,因此这次更新没有找到合适的窗口作为焦点窗口。
5.2 关键点2
接着是移除ActivityRecord的流程:
在这里我们才看到将launcher的ActivityRecord.mVisibleRequested可见性改为true,此流程下有一次更新焦点窗口的机会:
ActivityRecord.setVisibility
-> ActivityRecord.commitVisibility
-> WMS.updateFocusedWindowLocked
但是由于此时由于处于transition collecting的阶段,因此最终提前返回了,没有继续调用ActivityRecord.commitVisibility,ActivityRecord.commitVisibility中是有机会继续调用WMS.updateFocusedWindowLocked来更新焦点窗口的。
5.3 关键点3
最后是Launcher对应的WindowState走WMS.relayoutWindow:
本来也是有机会调用WMS.updateFocusedWindowLocked:
但是由于Launcher对应的窗口在relayoutWindow前后WindowState.mViewVisibility都是View.VISIBLE,原因则是WindowState.mViewVisibility改变的地方只有一处,在WindowState.setViewVisibility:
void setViewVisibility(int viewVisibility) {
mViewVisibility = viewVisibility;
}
该方法只在WMS.relayoutWindow中调用:
win.setViewVisibility(viewVisibility);
ProtoLog.i(WM_DEBUG_SCREEN_ON,
"Relayout %s: oldVis=%d newVis=%d. %s", win, oldVisibility,
viewVisibility, new RuntimeException().fillInStackTrace());
而”com.example.demoapp.MainActivity“启动后又迅速挂掉了,没来得及改变Launcher对应的窗口WindowState.mViewVisibility,因此认为不存在焦点的切换,最后也是没有去更新焦点窗口。
从log上也能看到Launcher对应的窗口只在Demo App挂掉后relayout了两次,且每次传入的viewVisibility都是View.VISIBLE:
因此在Launcher的窗口走WMS.relayoutWindow流程的时候,也错失了更新焦点窗口的机会,导致后续焦点窗口一直都是空了。