99%面试必问题目:讲一下ANR吧!

1.什么是ANR

ANR(Application Not responding)是指应用程序未响应,主线程如果在规定时间内没有处理完相应的工作,就会出现ANR,ANR本质上是性能问题,ANR机制实际上是对应用程序的主线程起到了限制作用。

2.ANR产生的原因是什么?

主线程(UI线程)如果在规定时间内没有处理完相应的工作,就会出现ANR,具体的说就是:

1. 输入事件(按键和触摸事件)5s内没有被处理:Input event dispatching timed out
2. BroadcaseReceiver的事件(onReceive方法)在规定时间内没处理完(前台广播10s,后台广播60s):Timeout of broadcast BroadcastRecord
3. service前台20s,后台200s没有完成启动 :Timeout executing service
4. ContentProvider的publish在10s内没进行完:Timeout publishing content providers

3.ANR发生的原理 -- 机制

ANR机制可以分为两部分:

  • ANR监测机制:Android对于不同的ANR类型(Broadcast, Service, InputEvent)都有一套监测机制。
  • ANR报告机制:在监测到ANR以后,需要显示ANR对话框、输出日志(发生ANR时的进程函数调用栈、CPU使用情况等)。

整个ANR机制的代码也是横跨了Android的几个层:

  • App层:应用主线程的处理逻辑;
  • Framework层:ANR机制的核心,主要有AMS、BroadcastQueue、ActiveServices、InputmanagerService、InputMonitor、InputChannel、ProcessCpuTracker等;
  • Native层:InputDispatcher.cpp;

直白的说就是程序在执行相关操作的时候,会通过handler发送一个延时消息,也就是ANR消息,延时的时间就是不同组件发生ANR的时间,当我们进行相关操作之后,会romove掉该条message,如果相关的操作没有在规定的时间内完成,那么Handler就会执行该条message,就发生的ANR。

4.ANR定位 - 如何找到ANR发生的原因

发生ANR的原因有以下几点:

  1. 主线程在做一些耗时任务
  2. 主线程被其他线程锁
  3. cpu被其他进程占用,该进程没被分配到足够的cpu资源

如何准确的找到ANR发生的位置,就需要我们结合具体的场景进行分析,具体可以从以下几个方面进行分析

  1. 可以在log中搜索“ANR in”或“am_anr”,会找到ANR发生的log,该行会包含了ANR的时间、进程、是何种ANR等信息,如果是BroadcastReceiver的ANR,可以怀疑是BroadcastReceiver.onReceive()方法的问题。如果是Service或Provider就怀疑是否其onCreate()的问题。

  2. 在该条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总用量不高,该进程和其他进程的占用过高,这有一定概率是由于某些主线程的操作就是耗时过长,或者是由于主进程被锁造成的。

  3. 除了上述的情况(1)以外,分析CPU usage之后,确定问题需要我们进一步分析trace文件。trace文件记录了发生ANR前后该进程的各个线程的stack。对我们分析ANR问题最有价值的就是其中主线程的stack,一般主线程的trace可能有如下几种情况:
    (1). 主线程是running或者native而对应的栈对应了我们应用中的函数,则很有可能就是执行该函数时候发生了超时。
    (2). 主线程被block:非常明显的线程被锁,这时候可以看是被哪个线程锁了,可以考虑优化代码。如果是死锁问题,就更需要及时解决了。
    (3). 由于抓trace的时刻很有可能耗时操作已经执行完了(ANR -> 耗时操作执行完毕 ->系统抓trace),这时候的trace就没有什么用了,主线程的stack就是这样的:

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
  | sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
  | state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
  | stack=0xbe307000-0xbe309000 stackSize=8MB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/3276/stack)
  native: #00 pc 0004099c  /system/lib/libc.so (__epoll_pwait+20)
  native: #01 pc 00019f63  /system/lib/libc.so (epoll_pwait+26)
  native: #02 pc 00019f71  /system/lib/libc.so (epoll_wait+6)
  native: #03 pc 00012ce7  /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
  native: #04 pc 00012f63  /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
  native: #05 pc 00086abd  /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
  native: #06 pc 0000055d  /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:323)
  at android.os.Looper.loop(Looper.java:138)
  at android.app.ActivityThread.main(ActivityThread.java:5528)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)

当然这种情况很有可能是由于该进程的其他线程消耗掉了CPU资源,这就需要分析其他线程的trace以及ANR前后该进程自己输出的log了。

5.ANR的修正 - 如何避免ANR

  1. 主线程读取数据:在Android中主线程去读取数据是非常不友好的,Android从2.3之后是不允许主线程从网络读取数据的,但系统允许主线程从数据库或者其他地方读取数据,但是这种情况就会容易产生ANR。
    • 避免在主线程执行query provider,首先这会比较耗时,另外这个操作provider那一方如果挂掉的话或者正在启动,那我们应用的query就会很长时间不会返回,我们应该在其他线程中执行数据库的query,provider的query等获取数据的操作。
    • sharePreference的调用。sharePreference的commit()方法是同步的,apply()方法一般是异步执行。所以在主线程不要用commit()方法,用apply()替换。SharedPreference的写是全量写而非增量写。所以尽量都修改完然后再调用apply(),apply()方法再Activity stop的时候,主线程会等待写入完成,提交多次就会容易造成卡顿。并且存储文本不宜过大,这样会很慢。另外,如果写入的是json或者xml,由于需要加和删转义符号,速度会比较慢。
  2. 尽量避免在BroadcastReceiver的onReceive()方法中执行操作,特别是应用在后台的时候,为了避免这种情况,一种解决方案是直接开启异步线程去执行任务,但是此时应用可能在后台,系统优先级较低,进程很容易就被杀死了,所以可以选择开启Service去执行任务。
  3. 各个组件的生命周期函数都不应该有太耗时的操作,即使对于后台Service或者ContentProvider来讲,应用在后台运行时候其onCreate()时候不会有用户输入引起事件无响应ANR,但其执行时间过长也会引起Service的ANR和ContentProvider的ANR。
  4. 尽量避免主线程的被锁的情况,在一些同步的操作主线程有可能被锁,需要等待其他线程释放相应锁才能继续执行,这样会有一定的ANR风险,对于这种情况有时也可以用异步线程来执行相应的逻辑。另外, 我们要避免死锁的发生(主线程被死锁基本就等于要发生ANR了)。

参考资料
ANR监测机制
Android ANR问题总结

推荐阅读更多精彩内容