Android全称是Application Not Response,即程序无响应。ANR的直观体验是用户在操作APP的过程中,感觉界面卡顿,如果 Android应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误,如下图所示,ANR 对话框会为用户提供强行退出应用的选项。
当点击了Close app或者由于ANR引起了闪退之后,这时查看Logcat,一般可以发现ANR以及trace.txt等字样,可以知道出现ANR主要原因是我们在主线程做了太多耗时操作,这时你可以选择等待按钮,等待应用程序结束主线程的耗时操作,或者选择确定按钮,结束这个应用程序,ANR对于一个应用来说是不能承受之痛,其影响并不比应用发生Crash小。
出现ANR的一般有以下几种类型:
从应用的角度上来讲,它的原因可以归结为以下几种:
开发中由于个人原因,多多少少都会可能写出造成ANR的代码,要想在开发中及时发现问题,可以使用StrictMode有助于您在开发应用时发现主线程上的意外 I/O 操作。
严苛模式是一个避免你不小心把网络或者IO操作放在UI线程操作的开发工具,从而实现避免ANR。使用严苛模式,系统检测出主线程违例的情况会做出相应的反应,如日志打印,屏幕闪烁(需要在开发者选项里面打开启用严格模式)等。
只有在设备的开发者选项中启用了显示所有 ANR 时,Android 才会针对花费过长时间处理广播消息的应用显示 ANR 对话框。我们可以通过打开这个选项,在开发中及早发现相关问题。
TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。(注意:Android Studio3.2之后已经弃用)
Android Studio3.2之后,CPU Profiler替代了TraceView,我们可以通过在通过记录应用交互过程获取相关方法执行顺序和耗时图,从而分析哪些方法耗时过长导致ANR。
在我们日常开发中经常遇到的ANR问题都是线上用户使用时发现的ANR问题,如果是我们开发中就已经发现,那是非常好解决的,只需要聚焦于新增代码块即可,但是如果是针对线上版本,那么我们就需要对线上数据进行监控,很多公司都会自主研发APM系统,或者是借用第三方的,抑或使用Google官方的,都是可以统计到ANR的数据。其实有时候发现站在巨人的肩膀上去做一些事情,也许效率会更高,以下是一些常用的ANR数据收集工具:
国外
国内
无论我们通过线上还是开发中发现了ANR问题,我们都需要思考怎么去解决。
1、修改主线程上耗时的代码
通过TraceView或者CPU Profiler可以找到应用中主线程忙碌时间超过5s的位置,然后把此处代码移到子线程操作。如一些网络操作、耗时计算等。
2、锁的竞争导致堵塞
如果主线程参与锁的竞争,有可能会导致主线程持续等待锁而造成堵塞的问题,从而引发ANR。所以最好还是避免主线程出现竞争锁的情况。
3、死锁
如果某资源被另一个线程所持有,而该线程又在等待主线程的资源,就会陷入死锁情况导致ANR。
4、广播接收器执行速度慢
如在广播接收器的onReceive()执行了长时间的耗时操作,就会可能导致ANR,所以应该把耗时操作放到子线程操作。
站在巨人的肩膀固然好,但是如果能够自己变成巨人那就更好了,所以作为一个有追求的开发者,我们当然要思考如何监控APP的ANR问题。
目前流行的ANR检测方案有开源的BlockCanary 、ANR-WatchDog、SafeLooper,xCrash, 还有根据谷歌原生系统接口监测的方案:FileObserver。
BlockCanary是Android平台上的一个轻量的,非侵入式的性能监控组件,可以在使用应用的时候检测主线程上的各种卡顿问题,并可通过组件提供的各种信息分析出原因并进行修复。
原理:
(1) 在Looper里面的loop方法里面有一个Printer打印日志,它在每个Message处理的前后被调用,而如果主线程卡住了,就是dispatchMessage里卡住了。
public static void loop() {
// ....
for (;;) {
// ...
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// ...
msg.target.dispatchMessage(msg);
// ...
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// ...
}
}
(2) 每个线程只有一个Looper,而只要重新设置主线程Looper里面的Printer即可重写println方法,然后在这里面进行前后两次消息处理的时间差,从而计算是否有发生ANR。
// 创建自定义Printer
...
@Override
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
...
(3)设置自定义Printer到主线程Looper
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
优点:灵活配置可监控常见APP应用性能也可作为一部分场景的ANR监测,并且可以准确定位ANR和耗时调用栈。
缺点:
ANR-WatchDog是参考Android WatchDog机制,起了个单独线程向主线程发送一个变量+1的操作,然后休眠一定时间阈值(阈值可自定义,例如5s),休眠过后再判断变量是否已经+1,如果未完成则ANR告警。在GP上有有2.68%实用率的ACRA库也推荐使用这种方式。
SafeLooper是一个第三方开源的库,主要用于捕获未知异常,避免出现ANR弹窗,从而在捕获到异常时获取ANR信息。
通过复现ANR场景,可以发现/data/anr文件夹会伴随ANR发生而变化,所以可以通过监听/data/anr目录下是否有文件写入,如果有的话就认为发生了ANR,像Bugly对ANR的监控就是用这种方法来实现的。
xCrash是爱奇艺开源的一个性能监控SDK,它的实现方案和以上四种不太一样,它是通过监控系统的信号量的变化,从而确定是否发生了ANR,然后再整理输出Tombstone文件。由于我对系统信号量了解不多,这里就贴出官方文档的原理图,欢迎了解的大神补充。