【问题分析】ANR

用户操作出的 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代码:

java 复制代码
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:

java 复制代码
    void setViewVisibility(int viewVisibility) {
        mViewVisibility = viewVisibility;
    }

该方法只在WMS.relayoutWindow中调用:

java 复制代码
            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流程的时候,也错失了更新焦点窗口的机会,导致后续焦点窗口一直都是空了。

相关推荐
Estar.Lee4 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯42 分钟前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!4 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟4 小时前
Android音频采集
android·音视频
小白也想学C6 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程6 小时前
初级数据结构——树
android·java·数据结构
闲暇部落8 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX10 小时前
Android 分区相关介绍
android
大白要努力!11 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle