产生的原因及其定位分析
ANR 是Android中一个独有的概念,每一本介绍Android开发的入门书几乎都会对其进行介绍,它的全称是Application Not Responding(应用程序无响应),对于高质量的代码,ANR在开发者自测过程中可能不会经常遇到,但一旦测试人员进行Monkey测试,AND出现的概率就比较高了,如何快速分析定位并解决,是开发者的必修课。
ANR的直观体验是用户在操作APP的过程中,感觉界面卡顿,比如按下某个按钮,打开某个页面等,当卡顿超过一定时间(一般是5秒)时就会出现ANR对话框。
这时查看Logcat,一般可以发现ANR以及trace.txt等字样,后面会详细介绍,可以发现出现ANR主要是因为我们在主线程做了太多耗时操作,这时你可以选择等待按钮,等待应用程序结束主线程的耗时操作,或者选择确定按钮,结束这个应用程序,ANR对于一个应用来说是不能承受之通,其影响并不比应用发生Crash小。
ANR产生的原因
只有当应用程序的UI线程响应超时才会引起ANR,超时产生原因一般有两种。
1 当前的事件没有机会得到处理,例如UI线程正在响应另外一个事件,当前事件由于某种原因被阻塞了。
2 当前的事件正在处理,但是由于耗时太长没能及时完成。
根据ANR产生的原因不同,超时时间也不尽相同,从本质上讲,产生ANR的原因有三种,大致可以对应到Android 中四大组件中的三个(Activity/View,BroadcastReceiver和Service)。
KeyDispatchTimeOut :最常见的一种类型,原因是View的按键事件或者触摸事件在特定的时间(5秒)内无法得到响应。
BroadcaseTimeOut:原因是BroadcastReceiver的onReceiver()函数运行在主线程中,在特定的时间(10秒)内无法完成处理。
ServiceTimeOut:比较少出现的一种类型,原因是Service的各个生命周期函数在特定时间(20秒)内无法完成处理。
典型的ANR问题场景
1 应用程序UI线程存在耗时操作,例如在UI线程中进行网络请求,数据库操作或者文件操作等,可能会导致UI线程无法及时处理用户输入等,当然在Android 4.0之后,如果在UI线程中进行网络操作,将会抛出NetworkOnMainException异常。
2 应用程序的UI线程等待子线程释放某个锁,从而无法处理用户的输入
3 耗时的动画需要大量的计算工作,可能导致CPU负载过重
ANR的定位和分析
当发生ANR时,开发者可以通过结合Logcat日志和生成的位于手机内部存储的/data/anr/traces.txt文件进行分析和定位。
1 Logcat日志信息
下面我们通过在主线程中模拟一个耗时操作来使应用发生ANR,除了在Logcat中可以看到生成traces.txt文件的日志外。
同时如果由于ANR导致应用发生崩溃,那么除了打印出上面的信息之外,还会调用CrashAnrDetector打印出下面的信息:
可以看到,Logcat日志信息中主要包含如下内容:
1 导致ANR的类命及所在包名:MainActivity,com.asce1885.anrdemo
2 发生ANR的进程名称及ID: com.asce1885.anrdemo,8672
3 ANR产生的原因(类型):Input disptching time out ,属于KeyDispatchTimeOut类型
4 系统中活跃进程的CPU占用率:1.3% 8672/com.asce1885.anrdemo:0.7% user+0.6% kernel/faults:7276 minor 17 major.
2 Traces.txt日志信息
从Logcat 的日志信息我们可以知道引发ANR的具体的类信息,以及ANR的类型,但是这不足够开发者定位到具体引发问题的代码行,为了获得进一步的信息,我们需要借助于ANR过程中生成的堆栈信息文件/data/anr/traces.txt ,这个文件可以通过终端Termianl中执行adb pull命令从手机的内部存储中拷贝到电脑中,也可以通过如下语句拷贝到MacBook Pro的桌面上
可以看到 traces.txt文件有助于问题定位的信息主要包括如下内容
发生ANR的进程名称,ID,以及时间
手机的CPU架构:arm
堆内存信息:24%free ,16MB/21MB,53963object
主线程基本信息:
线程名称=”main“ 线程优先级:prio=5
线程锁ID:tid=1 线程状态:Sleeping
主线程的详细信息
线程组名称:group =”main“
线程被挂起的次数:sCount=1
线程被调试器挂起的次数:dsCount=0,当线程被调试结束后,sCount会被重置为0,它的值会重新根据是否被挂起而增加,而dsCount不会被重置为0,因此,dsCount可以用来判断这个线程是否被调试器调试过。
线程的Java对象地址:obj=0x87789ef0
线程本身的Native对象地址:self=0xb4f07800
线程的调度信息
Linux系统中内核线程ID:sysTid=8672,可以看到主线程的线程号和进程号相同
线程调度优先级:nice=0
线程调度组:cgrp=apps
线程调度策略和优先级:sched=0/0
线程处理函数地址:handle=0xb6fe4ec8
线程的上下文信息
线程调度状态:state=s
线程在CPU中的执行时间,线程等待时间,线程执行的时间片长度:schedsta=(...)
线程在用户态中调度时间值:utm=18
线程在内核态中的调度时间值:stm=6
最后执行这个线程的CPU核序号:core=3
线程的堆栈信息
堆栈地址和大小:stack=... stackSize=8MB
堆栈信息:从中可以看到,ANR是由于在MainActivity类中的triggerAnrWithLongOperation函数调用了Threadsleep导致
ANR的避免和检测
为了避免在开发中引入可能导致应用发生ANR的问题,除了切记不要在主线程中作耗时操作,我们也可以借助于一些工具来进行检测,从而更有效的避免ANR的引入
StrictMode
严格模式StrictMode是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode 主要检测两大类问题
1 线程策略ThreadPolicy:
detectCustomSlowCalls:检测自定义耗时操作
detectDiskReads:检测是否存在磁盘读取操作
detectDiskWrites:检测是否存在磁盘写入操作
detectNetwork:检测是否存在网络操作
2 虚拟机策略VmPolicy
detectActivityLeaks:检测是否存在Activity泄漏
detectLeakedClosableObjects:检测是否存在未关闭的Closable对象泄漏
detectLeakedSqliteObjects:检测是否存在Sqlite对象泄漏
setClassInstanceLimit:检测类实例个数是否超过限制
可以看到,其中的ThreadPolicy可以看来检测可能存在的主线程耗时操作,解决这些检测到的问题能够减少应用发生ANR的概率,需要注意的是,我们只能在Debug版本中使用它,发布到市场上的版本要关闭掉,StrictMode的使用简单,我们只需要在应用初始化的地方例如Application或者MainActivity类的onCreate 方法中执行如下代码即可
上面的初始化代码调用penaltyLog表示在Logcat中打印日志,调用detectAll方法表示启动所有的检测策略,我们也可以根据应用的具体需求只开启某些策略
BlockCanary
BlockCanary是一个非侵入式的性能监控函数库,它的用法和LeakCanary类似,只不过后者监控应用的内存泄漏,而BlockCanary主要用来监控应用主线程的卡顿,它的原理是利用主线程的消息队列处理机制,通过对比消息分发开始和结束的时间点来判断是否超过设定的时间,如果是,则判断为主线程卡顿,它的集成很简单,首先在build.gradle中添加在线依赖
然后在Allpication类中进行配置和初始化即可
网友评论