大部分做安卓的小伙伴相信对于ANR一定不陌生,相比于发生应用程序崩溃,发生ANR更加让人头大,主要原因是崩溃发生的时候会在Logcat中打印出发生异常的位置,开发人员很容易就能定位到崩溃并解决,显然ANR没那么轻松;但是我们大可不必这么忧伤,因为有问题就会有解决办法,解决不了,只是因为没有用对方法;

首先要搞定ANR就要对他有一个根本性的认识,和我们了解任何事物一样,只有抓住了事物之根本,才能在应对各种各样复杂的场景时保持镇定,运筹帷幄(嫌我啰嗦直接拉到后面看案例);

认识问题

那什么是ANR呢?我总结过来说就是Google为了让应用足够快的响应用户引入的一套机制,当主线程的UI绘制变慢的时候就可能导致ANR异常;不过这些基础知识对于各位看官来说都是小儿科了,我们就直奔主题,是什么导致应用程序出现ANR异常,而我们又应该怎么灵巧应对这些异常呢?

我这里给出一个官方的解释,其实我很鼓励大家在遇到问题的时候先看官方文档,因为官方的解释最能抓住问题的根本,与其说这是一种方法,倒不如说是一种解决问题的思维方式;
在这里插入图片描述
官方标出了重点,就是不要在主线程上做耗时操作,不过我们在解Bug的时候发现远比这一句话来得复杂,因为哪怕我们没有在主线程上做过多的操作,我们也会遇到ANR问题,这个是我们今天要解决的重点;

我简单的把这些场景分为4类:

  1. KeyDispatchTimeout(主要类型)
    按键或触摸事件在特定时间内无响应,谷歌default 5s,MTK平台上是8s;其实这个很好理解,第一种是和用户交互的时候出现的,能和用户直接交互的界面就是运行在主线程上,当程序无法响应用户的交互事件,自然会抛出来ANR;
  2. BroadcastTimeout(10s)
    BroadcastReceiver在特定时间内无法处理完成,因为Broadcast是跑在主线程上的,而且Broadcast的生命周期只有10s,10s没有做完就会被安卓系统认为出现了ANR异常;
  3. ServiceTimeout(20s) --小概率类型
    Service在特定的时间内无法处理完成,Service是跑在主线程上的,这种情况出现的不多,我相信大部分开发人员能规避这种情景;
  4. 主线程被杀的ANR
    如果程序运行时产生了运行时的异常,本来应该显示程序异常运行,偏偏我们为了提高使用体验做了一个异常捕捉器,而这个异常是主线程跑出来的,于是主线程被杀,ANR产生(其实这并不完全叫做ANR了);

定位问题

软件问题发生的时候,根据场景我们可以分为生产线场景和开发场景;

生产线场景指的是外场在使用发布版本的时候,发生了ANR异常,而通常外场不方便赶回来或者将机器寄回来,只能通过查看手机里面的相关日志来定位解决问题;

开发场景指的是还在开发尚未发布的阶段,开发人员可以使用电脑和手机连接进行调试以及查看相关的调试信息,这无疑比外场环境方便不少;

只不过外场发现的Bug通常是偶现的,那么开发人员想要复现是一件比较麻烦的事情,不过怎么样复现问题大家会有自己就的方法,这里主要介绍如何在复现问题后提供最有效的解决方案;

外场发现的问题,如果可以通过日志定位到,就不需要通过开发人员复现来定位,因此再出现外场问题的时候推荐先查看外场手机的日志,如果日志无法定位问题,再想办法在开发场景复现;

外场问题的解决分为以下几个关键步骤:

  1. 联系外场人员,将手机的程序运行日志(发布版程序都会将关键日志信息打印到文件,如果没有则直使用adb logcat 指令导出)拷贝出来发送给开发人员;
  2. 联系外场人员使用手机通过adb连接电脑,使用BugReport命令导出手机系统运行日志并发给开发人员;
  3. 开发人员分析程序运行日志,看是否能在程序运行日志中发现ANR问题,如果不能则需要查看Bug Report导出的文件(具体分析步骤见下边);
  4. 如果在bugreport也无法定位问题,那么就需要想办法在开发环境中复现这个问题,开发环境中复现定位问题在后面会讲到;
  5. 开发环境在复现问题的同时通过辅助工具猜测可能的问题发生原因,并尝试打补丁后交给外场人员复现;

ANR分析一般步骤

BugReport会记录发生ANR的所有关键信息,一些环境相关的信息我们可以不需要反复询问现场测试人员就能看到。

分析步骤
1.查看一些版本和手机信息,确认问题的系统环境
2.查看CPU/MEMORY的使用状况,看是否有内存耗尽,CPU繁忙这样的背景情况出现.
3.分析traces,因为traces是系统出错以后输出的一些线程堆栈信息,可以很快定位到问题出在哪里.
4.分析SYSTEM LOG,系统Log详细输出各种log,可以找出相关log进行逐一分析

获取ANR日志

Google为了方便Android开发人员分析整个系统平台或者某个APP运行一段时间后的所有信息,专门开发了adb bugreport工具。开发人员可以使用adb bugreport命令获取系统运行的所有log信息(这个)。命令如下:

adb bugreport > bugreport_out.txt

同时,你也可以通过获取手机data/anr目录下的trace.txt

定位ANR日志

如果你只是想要找到最近发生的ANR,只要在bugreport文件中搜索即可,关键词为VM TRACES AT LAST ANR
若存在该ANR则输出相应traces,否则输出:

*** NO ANR VM TRACES FILE (/data/anr/traces.txt): No such file or directory 

正常情况下我们都会在anr发生的时候导出日志,当然还有不正常的时候咯,那就需要往下滑动找了。而java代码引发的ANR则通常可以把报名作为关键词来搜索实现定位;

日志无法定位问题的解决方案

虽然第一类也是属于这种,但是区别在于这一类问题一般发生在应用运行过程中,这种情况是ANR类型问题里遇到最多的,比如网络访问,访问数据库之类的,都很容易造成主线程堵塞;
我解决这类问题的时候不是直接看tractView.txt,因为android studio提供了一个非常好的性能分析工具供我们跟踪这个问题,我们只需要打开android studio的性能分析页面:
在这里插入图片描述
进入到Cpu页面,第一个线程就是主线程,我们只要看主线程上cpu的占用(绿色块)就知道在什么时候要发生ANR了,因为发生ANR的时候,主线程会有很大块的绿色占用,我们使用录制工具,录制这段时间的cpu占用,就可以很快定位到问题,这一类的教程很多,我这里不再赘述;

解决问题

产生ANR的情况有很多种,前面列出了四种场景,但是具体问题具体分析,每一个场景产生的本质虽然一样,但是诱因却是多种多样;

第一类:主线程占用时间过多

这个是最常见的,也是最容易检测到的;通常这种ANR我们可以通过一些辅助工具,比如在项目中加入BlockCanary就能很方便的看出来。不愿意引入这种第三方组件也没关系,我们可以通过Android Studio自带的线程分析工具就行,这个工具可以告诉你什么地方的什么方法占用了多长实践,非常直观和方便;步骤嘛,我简单列一下,这里就不贴图了:

  1. 选中需要调试的模块,点击两个调试按钮中间的那个profile app按钮(或者点击底部工具栏的profiler选项卡)。
  2. 选中调试的包,单击CPU视图,有一个record按钮,点击,就开始记录线程工作信息了。
  3. 再次点击stop按钮就能停止记录,as会自动帮你分析记录的文件,并给出一个结果视图;
  4. 结果试图选中top down选项卡,有一个main(),她下面的都是在主线程中调用的方法栈,旁边可以看到占用主线程时长,很方便。
  5. 找到那个占用主线程时间最长的方法,优化它(放到子线程或减少调用),就好了;

假如是现场报过来的Bug,就需要看ANR日志。以data/anr为例。找那些TimeWait标识的,关键词是“TIMED_WAITING”。其实这种虽然会卡主线程,但是不至于会导致卡死不能动,如果卡死不能动,那就是下面的情况;

第二类:死锁导致ANR

这类问题相比前两类要麻烦一些,很多初级开发是不知道的;
首先我们要确定ANR的发生时因为用户点击还是自己就发生了,因为如果测试人员没有点击就发生了ANR,那很可能是出现了死锁,死锁在日志上的表现是很直接的,因为我们知道死锁导致的ANR的根源是是因为一个线程持有了锁主线程在等待这个线程释放锁;当等待时间一场,必然发生ANR;

这种情况下我们直接在日志里面找下面这些字样:
在这里插入图片描述
大家看我标红的位置,一看就知道是这里主线程阻塞了,原因是在等待45号线程持有的锁释放;通常这个时候还会在上面打印出来是发生在哪一行代码中,这个时候我们定位到对应的代码,再去解决这个问题就相当容易;

第三类:卡IO

这种情况一般是和文件操作相关,判断是否是这种情况,可以看mainlog中搜索关键字"ANR in",看这段信息的最下边,比如下面的信息
ANRManager: 100% TOTAL: 2% user + 2.1% kernel + 95% iowait + 0.1% softirq
很明显,IO占比很高,这个时候就需要查看trace日志看当时的callstack,或者在这段ANR点往前看0~4s,看看当时做的什么文件操作,这种场景有遇到过,常见解决方法是对耗时文件操作采取异步操作;

第四类:Binder线程池被占满

这一类问题出现的很少见,但是少不代表没有;
系统对每个process最多分配15个binder线程,这个是谷歌的设计(/frameworks/native/libs/binder/ProcessState.cpp)
如果另一个process发送太多重复binder请求,那么就会导致接收端binder线程被占满,从而处理不了其它的binder请求
这本身就是系统的一个限制,如果应用未按照系统的要求来实现对应逻辑,那么就会造成问题。
而系统端是不会(也不建议)通过修改系统行为来兼容应用逻辑,否则更容易造成其它根据系统需求正常编写的应用反而出现不可预料的问题。
判断Binder是否用完,可以在trace中搜索关键字"binder_f",如果搜索到则表示已经用完,然后就要找log其他地方看是谁一直在消耗binder或者是有死锁发生
之前有遇到过压力测试手电筒应用,出现BInder线程池被占满情况,解决的思路就是降低极短时间内大量Binder请求的发生,修复的手法是发送BInder请求的函数中做时间差过滤,限定在500ms内最多执行一次;

第五类:主线程Binder调用等待超时

一般是上面的我发现都不好使了,我才会使用这种方案,大家看一下下面这段:
在这里插入图片描述
很明显当时在做Binder通信,并没有waiting to lock等代表死锁的字样,那么说明这个案例即有可能是在等Binder对端响应,我们知道Binder通信对于发起方来说是阻塞等待响应,只有有了返回结果后才会继续执行下去
所以,如上这个案例中需要找到对端是哪个进程,这个进程当时在做什么,这时候就需要找到anr文件夹下另外一个文件binderinfo,这里需要找到与我们发起方进程1461通信的是哪个进程;

在这里插入图片描述

可以看到是1666号这个进程,再回到trace中看下,这个进程当时在做什么

在这里插入图片描述

可以看到当时对端在做消息的读取,也就是说这里出了问题,很明显这里我们无法修改,我们这个问题在于主线程执行了Binder请求,对端迟迟未返回便很容易出现这个问题,当前做法异步中执行;

第六类:主线程被杀死

按理说主线程被杀死,程序就会立即退出;实际上程序并不是立即退出的,在实际排查问题的时候我发现,虽然主线程挂了,但是其他线程还能继续跑。但是UI已经无法操作了,直到整个程序重启;

这种情况你去看trace.txt和分析这个状态的主线程占用,你会发现nativePollOnce最可疑;

at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

当一个 Java 线程抛出了未捕获的异常时,JVM 先会调用到 UncaughtExceptionHandler,然后再会把此线程停止掉。所以这段代码中,如果主线程抛出异常,那么第 6 行的方法结束后,主线程就会被 JVM 给停止掉,既然主线程都停止掉了,那自然就无响应了,也就会发生 ANR 了。

事实上,一般我们设置自定义 UncaughtExceptionHandler 时,都会在自定义的 uncaughtException 方法最后再调用一遍被我们顶替掉的系统默认的 UncaughtExceptionHandler,以便把应用 kill 掉,而这个例子,充分的显示了,当未捕获异常发生后,就算赖着不 kill 掉应用也是不行的,因为可能主线程都已经被停掉了。

关于 JVM 先调用 UncaughtExceptionHandler 然后把发生未捕获异常的线程停止掉的说法,见于 Java Language Specification 11.3,如下所示的片段:
在这里插入图片描述

第七类:内存泄露

什么?内存泄漏会导致ANR?扯淡吧?但事实就是我们在用小米手机开发的时候确实遇到了这样的问题。现象是整个手机界面还在,进程也还在,但整个APP已经无法使用了,不管是主线程还是子线程都已经没有活动,点击界面也无响应;

于是我们使用adb logcat命令将所有日志输出到log.txt,然后查看崩溃日志。果然在奔溃的事件前后发现了OOM异常,c层的代码打印内存分配失败(可分配的内存不足分配),此时看了下bugreport中的内存信息,果然占用了1.7个G,因此初步判定是内存泄漏导致的崩溃,而ANR只是假象;

为了验证这个猜想,我使用了Profile工具查看内存的泄露情况,才发现native层的内存一直在随着时间增长,因此这个问题就变成了内存优化的问题了,随后不久,这个问题也迎刃而解。

总结

不管是ANR问题还是其他问题,首先要从本质出来,程序的实践是很简单的,因为规则够明确。比如ANR问题,知道了ANR的本质那么就能通过一些手段判定是不是ANR。好多难解的问题并不是因为它本身有多难,而是因为找错了方向和突破点。

关于ANR的问题及解决方案会有很多,也许你们遇到的不一样,如果你们遇到了,解决了,欢迎留言告诉我,我会在第一时间将文章更新让更多人看到。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐