ANR是Android中一个独有的概念,它的全称是Application Not Responding(应用程序无响应)。
ANR的直观体验是用户在操作APP的过程中,感觉界面卡顿,比如按下某个按钮,打开某个页面等,当卡顿超过一定时间(一般是5秒)时就会出现ANR对话框,如下图所示:
这时查看Logcat,一般可以发现ANR以及traces.txt等字样。可以发现,出现ANR主要是因为我们在主线程中做了耗时操作。这时你可以选择“等待”按钮,等待应用程序结束主线程耗时操作,或者选择“确定”按钮,结束这个应用程序。
1、ANR产生的原因
只有当应用程序的UI线程响应超时才会引起ANR,超时产生原因一般有两种:
· 当前的事件没有机会得到处理,列如UI线程正在响应另外一个事件,当前事件由于某种原因被阻塞了。
· 当前的事件正在处理,但是由于耗时太长没能及时完成。
根据ANR产生的原因不同,超时时间也不尽相同,从本质上讲,产生ANR的原因有三种,大致可以对应到Android中四大组件中的三个(Activity/View、BroadcastReceiver和Service)。
KeyDispatchTiemout
最常见的一种类型,原因是View的按键事件或者触摸事件在特定的时间(5秒)内无法得到响应。
BroadcastTiemout
原因是BroadcastReceiver的onReceive()函数运行在主线程中,在特定的时间(10秒)内无法完成处理。
ServiceTiemout
比较少出现的一种类型,原因是Service的各个生命周期函数在特定时间(20秒)内无法完成处理。
2、ANR时系统做了什么
1).弹出一个对话框
2).将ANR信息输出到traces.txt文件中
traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下
adb pull /data/anr
3).将ANR信息输出到Logcat中
3、典型的ANR问题场景
· 应用程序UI线程存在耗时操作,例如在UI线程中进行网络请求、数据库操作或者文件操作,可能会导致UI线程无法及时处理用户输入等。当然在Android4.0之后,如果在UI线程中进行网络操作,将会抛出NetworkOnMainThreadException异常。
· 应用程序的UI线程等待子线程释放某个锁,从而无法处理用户的输入。
· 耗时的动画需要大量的计算工作,可能导致CPU负载过量。
· 其它线程终止或崩溃导致主线程一直等待。
· service忙导致超时无响应。
· system server中发生WatchDog ANR。
· service binder的数量达到上限。
· 硬件操作(比如camera)。
4、ANR的定位和分析
当发生ANR时,开发者可以通过结合Logcat日志和生成的位于手机内部存储的/data/anr/traces.txt文件进行定位和分析。
5、ANR的避免和检测
1).避免在主线程执行耗时操作,所有耗时操作应新开一个子线程完成,然后再在主线程更新UI。
2).BroadcastReceiver要执行耗时操作时应启动一个service,将耗时操作交给service来完成。
3).避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。如果你的应用程序在响应Intent广 播时需要向用户展示什么,你应该使用Notification Manager来实现。
UI线程尽量只做跟UI相关的工作
耗时的工作(比如数据库操作,I/O,网络操作),采用单独的工作线程处理
用Handler来处理UIthread和工作thread的交互
UI线程,例如:
Activity:onCreate(), onResume(), onDestroy(), onKeyDown(), onClick(),etc
AsyncTask: onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel,etc
Mainthread handler: handleMessage(), post*(runnable r), etc
…
ANR分析:需要关注CPU/IO,trace死锁等数据。
为了避免在开发中引入可能导致应用发生ANR的问题,除了切记不要在主线程中作耗时操作。
6、我们也可以借助于一些工具来进行检测,从而更有效的避免ANR的引入。
6.1StrictMode
严格模式StrictMode是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode主要检测两大类问题:
下面是启用StrictMode的实例,可以在Application的OnCreate中添加,这样就能在程序启动的最初一刻进行监控了。
private void setStrictMode() {
if (Integer.valueOf(Build.VERSION.SDK) > 3) {
Log.d(LOG_TAG, "Enabling StrictMode policy over Sample application");
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll() // 这里可以替换为.detectDiskReads().detectDiskWrites().detectNetwork()。
// detectAll() 包括了磁盘读写和网络I/O
.penaltyLog() //打印logcat,当然也可以定位到dropbox,通过文件保存相应的log
.penaltyDeath()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
}
}
· 线程策略ThreadPolicy
- detectCustomSlowCalls: 检测自定义耗时操作。
- detectDiskReads: 检测是否存在磁盘读取操作。
- detectDiskWrites: 检测是否存在磁盘写入操作。
- detectNetwork: 检测是否存在网络操作。
线程相关策略,包括主线程访问网络、磁盘(现在手机中使用闪存)读写、慢代码的检测。我们能够分别检测(detect) 这些操作或允许(permit)这些操作。一旦出现了违规(violation),就会有相应的提示(比如 Log 显示)。我们可以使用 detectNetwork() 检测主线程网络访问,detectDiskReads() 和 detectDiskWrites() 检测主线程磁盘读写,detectCustomSlowCalls() 检测主线程自定义的慢代码。当然也可以使用 permitXXX() 允许这些操作。
自定义慢代码分析,是仅当访问调用类的时后才触发的,可以通过这种方法去监视运行缓慢的代码。当在主线程中调用时, 这些验证规则就会起作用去检查你的代码。比如,当你的应用在下载或者解析大量的数据时,你可以触发自定义运行速度慢代码的查询分析, 作用很大。StrictMode可以用于捕捉发生在应用程序主线程中耗时的磁盘、网络访问或函数调用,可以帮助开发者使其改进程序, 使主线程处理UI和动画在磁盘读写和网络操作时变得更平滑,避免主线程被阻塞的发生。
· 虚拟机策略VmPolicy
- detectActivityLeaks: 检测是否存在Activity泄漏。
- detectLeakedClosableObjects: 检测是否存在未关闭的Closeable对象泄漏。
- detectLeakedSqlLiteObjects: 检测是否存在Sqlite对象泄漏。
- setClassInstanceLimit: 检测类实例个数是否超过限制。
虚拟机相关的策略,包括 SQLite 或 SQLiteCursor 没关闭、实现 Closable 接口的类使用后没关闭 等。 我们可以使用detectLeakedSqliteObjects() 检测 SQLite 和 SQLiteCursor 内存泄漏(没关闭),detectLeakedClosableObjects() 检测实现 Closable 接口的对象内存泄漏。
我们能通过 StrictMode.getThreadPolicy() 和 StrictMode.getVMPolicy() 获得当前采取的 ThreadPolicy 和 VMPolicy。
你可以决定当一个异常发生时该发生什么样的事情,比如,使用StrictMode的penaltyLog()方法你可以在应用发生异常时查看adb logcat的输出。 penaltyLog()表示将警告输出到LogCat,你也可以使用其他或增加新的惩罚(penalty)函数,例如使用penaltyDeath()的话,一旦StrictMode消息被写到LogCat后应用就会崩溃。和ThreadPolicy不同的是,VmPolicy不能通过一个对话框提供警告。
因为设置发生在线程中,严苛模式(StrictMode)甚至能在从一个对象到另一个对象的控制流中找到违例事件。当违例发生, 你会惊奇地注意到代码正运行于主线程,而栈trace将帮助你发现它如何发生。于是你能单步调试解决问题,或是将代码移到它自己的后台线程, 或是就保持原来的处理方式。这都取决与你。当然,你可能希望适时关闭严苛模式(StrictMode),当你的程序作为产品发布时, 你可不希望它仅为了一个警告在你的用户手里崩溃。有两个方法可以关闭严苛模式(StrictMode),最直接的就是移除相应代码, 但这样做不利于持续开发的产品。你通常可以定义一个应用级别布尔变量来测试是否需要调用严苛模式(StrictMode)代码。 在发布产品前将这个值定义为FALSE。更优雅的方式是利用调试模式(debug mode)的特点,在AndroidManifest.xml中定义这个布尔变量。
字段的属性之一是android:debuggable,其义自明。 某些时候你不希望报告所有违例。那在主线程之外的其他线程中设置严苛模式(StrictMode)很不错。譬如,你需要在正在监视的线程中进行磁盘读取。 此时,你要么不去调用detectDiskReads(),要么在调用detectAll()之后跟一个permitDiskReads()。类似允许函数也适用于其他操作。 当你发现一个比较严重的异常时,Android提供了一系列的工具来解决它:线程、Handler、AsyncTask、IntentService等等。但并不是StrictMode 报的所有问题都需要修复,特别是很多必须要在窗口生命周期回调中访问磁盘的时候。使用严苛模式可以帮你解决很多问题,比如在UI线程中访问网络始终 是一个问题。 当应用启用了strictmode模式时,其实跟普通的应用没什么两样,在测试和运行时,跟平时运行普通应用程序一样就可以了。 当启用了Strictmode模式时,会监视所有的程序运行情况,当发现出现重大问题或违背策略规则时,会提示用户。 下面是当运行启用了strictmode模式的应用时,当发现违背规则时,显示给用户的信息。
6.2 BlockCanary
BlockCanary是一个非侵入式的性能监控函数库,它的用法和LeakCanary类似,只不过LeakCanary监控应用的内存泄漏,而BlockCanary主要用来监控应用主线程的卡顿。它的基本原理是利用主线程的消息队列处理机制,通过对比消息分发开始和结束的时间点来判断是否超过设定的时间,如果是,即判断为主线程卡顿。它的集成很简单,首先在build.gradle中添加在线依赖,如下:
dependencies {
compile 'com.github.moduth:blockcanary-android:1.2.1'
// 仅在debug包中启用BlockCanary进行卡顿监控和提示的话,可以这么用
debugCompile 'comgithub.moduth:blockcanary-android:1.2.1'
releaseCompile 'comgithub.moduth:blockcanary-no-op:1.2.1'
}
然后在Application类中进行配置和初始化即可,
public class DemoApplication extends Application {
@Override
public void onCreate() {
// 在主进程初始化调用
BlockCanary.install(this, new AppBlockCanaryContext()).start());
}
}