Android ANR:原理分析及解决办法
目录:
一、ANR说明和原因
二、ANR分析办法
三、如何降低ANR的概率
四、造成ANR的原因及解决办法
五、ANR源码分析
六、Android ANR的信息收集
一、ANR说明和原因
1.1 简介
ANR全称:Application Not Responding,也就是应用程序无响应。
1.2 原因
Android系统中,ActivityManagerService(简称AMS)和WindowManagerService(简称WMS)会检测App的响应时间,如果App在特定时间无法响应屏幕触摸或键盘输入时间,或者特定事件没有处理完毕,就会出现ANR。
以下四个条件都可以造成ANR发生:
-
InputDispatching Timeout:5秒内无法响应屏幕触摸事件或键盘输入事件
-
BroadcastQueue Timeout :在执行前台广播(BroadcastReceiver)的onReceive()函数时10秒没有处理完成,后台为60秒。
-
Service Timeout :前台服务20秒内,后台服务在200秒内没有执行完毕。
-
ContentProvider Timeout :ContentProvider的publish在10s内没进行完。
1.3 避免
尽量避免在主线程(UI线程)中作耗时操作。
那么耗时操作就放在子线程中。
关于多线程可以参考:Android多线程:理解和简单使用总结
二、ANR分析办法
从前文可以明确,ANR问题是由于主线程的任务在规定时间内没处理完任务,而造成这种情况的原因大致会有一下几点:
-
主线程在做一些耗时的工作
-
主线程被其他线程锁
-
cpu被其他进程占用,该进程没被分配到足够的cpu资源。
判断一个ANR属于哪种情况便是分析ANR问题的关键。那么拿到一个anr的日志,应该如何分析呢?
在发生ANR的时候,系统会收集ANR相关的信息提供给开发者:首先在Log中有ANR相关的信息,其次会收集ANR时的CPU使用情况,还会收集trace信息,也就是当时各个线程的执行情况。trace文件保存到了/data/anr/traces.txt中,此外,ANR前后该进程打印出的log也有一定价值。一般来说可以按一下思路来分析:
-
从log中找到ANR反生的信息:可以从log中搜索“ANR in”或“am_anr”,会找到ANR发生的log,该行会包含了ANR的时间、进程、是何种ANR等信息,如果是BroadcastReceiver的ANR可以怀疑BroadCastReceiver.onRecieve()的问题,如果的Service或Provider就怀疑是否其onCreate()的问题。
-
在该条log之后会有CPU usage的信息,表明了CPU在ANR前后的用量(log会表明截取ANR的时间),从各种CPU Usage信息中大概可以分析如下几点:
(1). 如果某些进程的CPU占用百分比较高,几乎占用了所有CPU资源,而发生ANR的进程CPU占用为0%或非常低,则认为CPU资源被占用,进程没有被分配足够的资源,从而发生了ANR。这种情况多数可以认为是系统状态的问题,并不是由本应用造成的。
(2). 如果发生ANR的进程CPU占用较高,如到了80%或90%以上,则可以怀疑应用内一些代码不合理消耗掉了CPU资源,如出现了死循环或者后台有许多线程执行任务等等原因,这就要结合trace和ANR前后的log进一步分析了。
(3). 如果CPU总用量不高,该进程和其他进程的占用过高,这有一定概率是由于某些主线程的操作就是耗时过长,或者是由于主进程被锁造成的。
-
除了上述的情况(1)以外,分析CPU usage之后,确定问题需要我们进一步分析trace文件。trace文件记录了发生ANR前后该进程的各个线程的stack。对我们分析ANR问题最有价值的就是其中主线程的stack,一般主线程的trace可能有如下几种情况:
(1). 主线程是running或者native而对应的栈对应了我们应用中的函数,则很有可能就是执行该函数时候发生了超时。
(2). 主线程被block:非常明显的线程被锁,这时候可以看是被哪个线程锁了,可以考虑优化代码。如果是死锁问题,就更需要及时解决了。
(3). 由于抓trace的时刻很有可能耗时操作已经执行完了(ANR -> 耗时操作执行完毕 ->系统抓trace),这时候的trace就没有什么用了,主线程的stack就是这样的:
16ad355cb45639f5.png
三、如何降低ANR的概率
有一些操作是很危险的,非常容易发生ANR,在写代码时候一定要避免:
-
主线程读取数据:在Android中主线程去读取数据是非常不好的,Android是不允许主线程从网络读数据的,但系统允许主线程从数据库或者其他地方获取数据,但这种操作ANR风险很大,也会造成掉帧等,影响用户体验。
(1)避免在主线程query provider,首先这会比较耗时,另外这个操作provider那一方的进程如果挂掉了或者正在启动,我们应用的query就会很长时间不会返回,我们应该在其他线程中执行数据库query、provider的query等获取数据的操作。
(2)sharePreference的调用:针对sharePreference的优化点有很多,文章http://weishu.me/2016/10/13/sharedpreference-advices/ 详细介绍了几点sharepreference使用时候的注意事项。首先sharePreference的commit()方法是同步的,apply()方法一般是异步执行的。所以在主线程不要用其commit(),用apply()替换。其次sharePreference的写是全量写而非增量写,所以尽量都修改完同一apply,避免改一点apply一次(apply()方法在Activity stop的时候主线程会等待写入完成,提交多次就很容易卡)。并且存储文本也不宜过大,这样会很慢。另外,如果写入的是json或者xml,由于需要加和删转义符号,速度会比较慢。
-
不要在broadcastReciever的onRecieve()方法中干活,这一点很容易被忽略,尤其应用在后台的时候。为避免这种情况,一种解决方案是直接开的异步线程执行,但此时应用可能在后台,系统优先级较低,进程很容易被系统杀死,所以可以选择开个IntentService去执行相应操作,即使是后台Service也会提高进程优先级,降低被杀可能性。
-
各个组件的生命周期函数都不应该有太耗时的操作,即使对于后台Service或者ContentProvider来讲,应用在后台运行时候其onCreate()时候不会有用户输入引起事件无响应ANR,但其执行时间过长也会引起Service的ANR和ContentProvider的ANR。
-
尽量避免主线程的被锁的情况,在一些同步的操作主线程有可能被锁,需要等待其他线程释放相应锁才能继续执行,这样会有一定的ANR风险,对于这种情况有时也可以用异步线程来执行相应的逻辑。另外, 我们要避免死锁的发生(主线程被死锁基本就等于要发生ANR了)。
四、造成ANR的原因及解决办法
上面例子只是由于简单的主线程耗时操作造成的ANR,造成ANR的原因还有很多:
- 主线程阻塞或主线程数据读取
解决办法:避免死锁的出现,使用子线程来处理耗时操作或阻塞任务。尽量避免在主线程query provider、不要滥用SharePreferenceS
- CPU满负荷,I/O阻塞
解决办法:文件读写或数据库操作放在子线程异步操作。
- 内存不足
解决办法:AndroidManifest.xml文件<applicatiion>中可以设置 android:largeHeap="true",以此增大App使用内存。不过不建议使用此法,从根本上防止内存泄漏,优化内存使用才是正道。
- 各大组件ANR
各大组件生命周期中也应避免耗时操作,注意BroadcastReciever的onRecieve()、后台Service和ContentProvider也不要执行太长时间的任务。
五、ANR源码分析
特别声明:文章 理解Android ANR的触发原理 分别记录了由Service、BroadcastReceiver和ContentProvider造成的ANR。下文引用该文代码,并依据自己的简单理解作总结。
https://www.jianshu.com/p/388166988cef
六、Android ANR的信息收集
无论是四大组件或者进程等只要发生ANR,最终都会调用AMS.appNotResponding()方法。