作者:Ryane_Lee
一、ANR的定义
Android全称是Application Not Response,即程序无响应。ANR的直观体验是用户在操作APP的过程中,感觉界面卡顿,如果
Android应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误,如下图所示,ANR 对话框会为用户提供强行退出应用的选项。
当点击了Close
app或者由于ANR引起了闪退之后,这时查看Logcat,一般可以发现ANR以及trace.txt等字样,可以知道出现ANR主要原因是我们在主线程做了太多耗时操作,这时你可以选择等待按钮,等待应用程序结束主线程的耗时操作,或者选择确定按钮,结束这个应用程序,ANR对于一个应用来说是不能承受之痛,其影响并不比应用发生Crash小。
二、ANR类型
出现ANR的一般有以下几种类型:
- KeyDispatchTimeOut :最常见的一种类型,原因是View的按键事件或者触摸事件在特定的时间(5秒)内无法得到响应。
- BroadcaseTimeOut:原因是BroadcastReceiver的onReceiver()函数运行在主线程中,在特定的时间(10秒)内无法完成处理。
- ServiceTimeOut:比较少出现的一种类型,原因是Service的各个生命周期函数在特定时间(20秒)内无法完成处理。
- ContentProviderTimeout:ContentProvider在10S内无法完成处理。
三、ANR原因
从应用的角度上来讲,它的原因可以归结为以下几种:
- 应用在主线程上非常缓慢地执行涉及 I/O 的操作。
- 应用在主线程上进行长时间的计算。
- 主线程在对另一个进程进行同步 binder 调用,而后者需要很长时间才能返回。
- 主线程处于阻塞状态,等待子线程的长时间耗时操作完成。
- 主线程在进程中或通过 binder 调用与另一个线程之间发生死锁。主线程不只是在等待长操作执行完毕,而且处于死锁状态。
三、如何检测和诊断ANR问题
1、开发中检测ANR
- StrictMode(严格模式)
开发中由于个人原因,多多少少都会可能写出造成ANR的代码,要想在开发中及时发现问题,可以使用StrictMode有助于您在开发应用时发现主线程上的意外
I/O 操作。
严苛模式是一个避免你不小心把网络或者IO操作放在UI线程操作的开发工具,从而实现避免ANR。使用严苛模式,系统检测出主线程违例的情况会做出相应的反应,如日志打印,屏幕闪烁(需要在开发者选项里面打开启用严格模式)等。
- 启用后台 ANR 对话框
只有在设备的开发者选项中启用了显示所有 ANR 时,Android 才会针对花费过长时间处理广播消息的应用显示 ANR
对话框。我们可以通过打开这个选项,在开发中及早发现相关问题。
TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace
文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。(注意:Android Studio3.2之后已经弃用)
Android Studio3.2之后,CPU
Profiler替代了TraceView,我们可以通过在通过记录应用交互过程获取相关方法执行顺序和耗时图,从而分析哪些方法耗时过长导致ANR。
2、线上ANR数据收集
在我们日常开发中经常遇到的ANR问题都是线上用户使用时发现的ANR问题,如果是我们开发中就已经发现,那是非常好解决的,只需要聚焦于新增代码块即可,但是如果是针对线上版本,那么我们就需要对线上数据进行监控,很多公司都会自主研发APM系统,或者是借用第三方的,抑或使用Google官方的,都是可以统计到ANR的数据。其实有时候发现站在巨人的肩膀上去做一些事情,也许效率会更高,以下是一些常用的ANR数据收集工具:
- 国外
- Google vitals:Google Play自带的性能统计工具。
- Firebase Crashlytics:Google Cloud Platform为应用开发者们提供的实时性能分析系统。
- ACRA:在Goolge Play上有2.68%实用率的ACRA库。
- 国内
- Bugly:腾讯Bugly项目组推出移动应用崩溃监控分析平台,提供崩溃、脚本错误、ANR(Android)/卡顿(iOS)问题等监控分析服务。
- xCrash:爱奇艺开源的一个性能监控的SDK。
- Umeng:国内移动统计分析服务平台,提供统计分析、更新、分享、推送等服务,其中,错误分析也是在统计分析的基础上添加。
- Testin:国内云测平台,服务升级后,提供云测,APM服务(包括性能监控,错误上报等),众测等服务。
三、如何解决问题
无论我们通过线上还是开发中发现了ANR问题,我们都需要思考怎么去解决。
1、修改主线程上耗时的代码
通过TraceView或者CPU Profiler可以找到应用中主线程忙碌时间超过5s的位置,然后把此处代码移到子线程操作。如一些网络操作、耗时计算等。
2、锁的竞争导致堵塞
如果主线程参与锁的竞争,有可能会导致主线程持续等待锁而造成堵塞的问题,从而引发ANR。所以最好还是避免主线程出现竞争锁的情况。
3、死锁
如果某资源被另一个线程所持有,而该线程又在等待主线程的资源,就会陷入死锁情况导致ANR。
4、广播接收器执行速度慢
如在广播接收器的onReceive()执行了长时间的耗时操作,就会可能导致ANR,所以应该把耗时操作放到子线程操作。
四、如何实现ANR监控
站在巨人的肩膀固然好,但是如果能够自己变成巨人那就更好了,所以作为一个有追求的开发者,我们当然要思考如何监控APP的ANR问题。
目前流行的ANR检测方案有开源的BlockCanary 、ANR-WatchDog、SafeLooper,xCrash,
还有根据谷歌原生系统接口监测的方案:FileObserver。
1、BlockCanary
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和耗时调用栈。
- 缺点:
- (1) 谷歌已经明确标注This must be in a local variable, in case a UI event sets the logger这个looger对象是可以被更改的,已经有开发者遇到在使用WebView时logger被set为Null导致BlockCanary失效,只能让BlockCanary在WebView初始化之后调用start。
- (2) 如果dispatchMessage执行的非常久是无法触发BlockCanary的逻辑。
- (3) 在Printer输出之前,有一段代码queue.next()也会可能发生ANR,从而造成无法监控到ANR。
- (4) 无法监控CPU资源紧张造成系统卡顿无法响应的ANR。
2、ANR-WatchDog
ANR-WatchDog是参考Android
WatchDog机制,起了个单独线程向主线程发送一个变量+1的操作,然后休眠一定时间阈值(阈值可自定义,例如5s),休眠过后再判断变量是否已经+1,如果未完成则ANR告警。在GP上有有2.68%实用率的ACRA库也推荐使用这种方式。
- 原理:如下图所示。
- 优点:
- (1) 兼容性好,无需适配机型。
- (2) 无需改动APP逻辑代码,非侵入性。
- (3) 性能影响不大。
- 缺点:
- (1) 无法保证能捕获所有ANR,对阈值设置影响捕获概率。如时间过长,中间发生的ANR则可能被遗漏掉。
3、SafeLooper
SafeLooper是一个第三方开源的库,主要用于捕获未知异常,避免出现ANR弹窗,从而在捕获到异常时获取ANR信息。
- 原理:SafeLooper其实就是一个Runnable,启动后把这个SafeLooper(消息)post到主线程,并且会轮训主线程的消息队列,通过反射把新消息进行处理,然后判断新消息的处理过程是否会出现ANR导致的异常,如果是就将其捕获,并把异常信息通过uncaughtException回调给用户进行处理。主要流程如下图所示。
- 优点:使用AOP思想进行异常捕获的思想值得借鉴。
- 缺点:使用反射会影响性能。
4、FileObserver
通过复现ANR场景,可以发现/data/anr文件夹会伴随ANR发生而变化,所以可以通过监听/data/anr目录下是否有文件写入,如果有的话就认为发生了ANR,像Bugly对ANR的监控就是用这种方法来实现的。
- 原理:自定义FileObserver监听/data/anr目录下文件是否有新增".tarce"结尾的文件,如果有则认为发生ANR,并导出trace文件,注意如果当多个APP同时发生ANR,里面会有多个trace文件,需要对包名时间等进行过滤。
- 优点:
- (1) 基于原生接口调用,时机和内容准确。
- (2) 无性能问题,实现简单。
- 缺点:
- (1) /data/anr目录可能不会在发生ANR时马上写入文件,可能会发生滞后,从而导致收集到的trace文件和线程信息不准确。
5、xCrash
xCrash是爱奇艺开源的一个性能监控SDK,它的实现方案和以上四种不太一样,它是通过监控系统的信号量的变化,从而确定是否发生了ANR,然后再整理输出Tombstone文件。由于我对系统信号量了解不多,这里就贴出官方文档的原理图,欢迎了解的大神补充。