Android ANR监测诊断以及解决办法

当UI线程阻塞时间太长,应用无响应(ANR)错误便会触发。如果应用位于前台,系统还会显示给用户一个ANR对话框,让用户有机会强制关闭应用。

ANR是一个问题,因为应用中负责更新UI的主线程无法处理用户输入事件或者绘制操作,从而导致用户感到沮丧。

一般分三种情况:

  • KeyDispatchTimeout(5 seconds):主要情况。按键或者触摸事件无法在特定时间内完成。
  • BroadcastTimeout(10 seconds) :BroadcastReceiver在特定时间内无法处理完成
  • ServiceTimeout(20 seconds): Service在特定的时间内无法处理完成(所以虽然service是后台执行的,但是他是运行在UI线程的,如果处理一些耗时操作,会造成ANR)
  • 监测和诊断问题

    如果应用已经发布了, Android vitals 可以向你警告ANR问题的发生。(PS:前提是要发布到Google Play Store上,国内如果不是面向海外的,可以集成友盟SDK或者腾讯Bugly等)

    Android vitals

    Google Play Console的Android vitals模块统计了应用的性能相关情况,包括ANR和Crash等。可以根据上面的统计来分析解决ANR和Crash问题。

    诊断ANR

    诊断ANR可用的常规套路:

  • 在主线程中执行IO操作
  • 在主线程执行长时间的计算
  • 主线程执行同步Binder操作访问另一个进程,该进程执行很长时间再返回
  • 非主线程持有lock,导致主线程等待lock超时
  • 主线程和另一个线程发生死锁,可以是位于当前进程或者通过Binder调用。
  • 用下面的技巧帮助你分析到底是上面的哪种情况引起了ANR。

    Strict mode

    使用 StrictMode 帮你找到主线程哪里调用了IO操作。这个模式打开后,可以尽可能帮助你找到主线程中的磁盘访问和网络访问操作,网络访问操作是肯定需要放到子线程中执行的。而磁盘操作的话,通常都会执行很快,当然能做到子线程中最好。官方文档也说了,不要强迫修复 StrictMode 帮你找到的所有内容,特别是,在正常的Activity生命周期中,许多磁盘访问操作是需要的。

    But don't feel compelled to fix everything that StrictMode finds. In particular, many cases of disk access are often necessary during the normal activity lifecycle

    调试模式下打开,但是发布状态不允许打开。

    可以在Activity或者Appliction的onCreat()方法中打开:

    public void onCreate() {
         if (DEVELOPER_MODE) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectDiskReads()
                     .detectDiskWrites()
                     .detectNetwork()   // or .detectAll() for all detectable problems
                     .penaltyLog()
                     .build());
             StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                     .detectLeakedSqlLiteObjects()
                     .detectLeakedClosableObjects()
                     .penaltyLog()
                     .penaltyDeath()
                     .build());
         super.onCreate();
    

    打开后台ANR弹框

    设备的开发者选项中可以打开“显示所有ANR”。默认Android只对于前台应用发生ANR时弹框,打开后,后台应用发生ANR也会弹框。

    TraceView

    使用TraceView分析应用运行时你根据用例情况在卡顿前后捕捉的方法调用trace文件。trace文件可以通过代码调用生成,或者通过Android Studio捕捉。

    Debug.startMethodTracing("hellotrace");    //开始 trace,保存文件到 "/sdcard/hellotrace.trace"
        // ...
    Debug.stopMethodTracing();    //结束
    

    使用adb命令将trace文件导出到电脑,然后放到DDMS中打开分析

    adb pull /sdcard/hellotrace.trace /tmp
    

    拉取traces文件

    当ANR发生时,Android系统会将一些trace信息存储到设备的/data/anr/traces.txt文件中。你可以利用adb命令将其拉取出来分析(前提是要root?不记得了)。

    对于模拟器,简单快速查看:

    adb root
    adb shell
    cat /data/anr/traces.txt
    

    还可以用bugreport命令导出。

    主线程执行慢代码

    将耗时操作异步执行。

    主线程执行IO操作

    建议将所有IO操作放到子线程执行。

    工作线程持有主线程需要获取某个资源的锁又不能及时释放的情况。

    通常发生ANR时,主线程处于Monitor或者BLOCKED状态。例子:

    @Override
    public void onClick(View v) {
        // The worker thread holds a lock on lockedResource
       new LockTask().execute(data);
       synchronized (lockedResource) {
           // The main thread requires lockedResource here
           // but it has to wait until LockTask finishes using it.
    public class LockTask extends AsyncTask<Integer[], Integer, Long> {
       @Override
       protected Long doInBackground(Integer[]... params) {
           synchronized (lockedResource) {
               // This is a long-running operation, which makes
               // the lock last for a long time
               BubbleSort.sort(params[0]);
    

    上述代码,主线程需要获取lockedResource锁,而该锁被LockTask持有,其sort方法为耗时操作,导致不能及时释放锁,从而引发主线程阻塞超时,导致ANR。

    另外一个例子,主线程等待子线程执行结果超时:

    public void onClick(View v) {
       WaitTask waitTask = new WaitTask();
       synchronized (waitTask) {
           try {
               waitTask.execute(data);
               // Wait for this worker thread’s notification
               waitTask.wait();
           } catch (InterruptedException e) {}
    class WaitTask extends AsyncTask<Integer[], Integer, Long> {
       @Override
       protected Long doInBackground(Integer[]... params) {
           synchronized (this) {
               BubbleSort.sort(params[0]);
               // Finished, notify the main thread
               notify();
    

    这些情况需要评估锁耗时,保证锁尽可能占有最少的时间。或者移除锁。

    尽可能避免死锁。

    执行缓慢的Broadcast Receiver

    当应用花费了太长时间处理广播消息时候也会导致ANR发生。

    下面的情况会导致ANR发生:

  • 广播接收者没能及时执行完成onReceive()方法(通常10s)
  • 广播接收者调用了goAsync()方法,但是没有调用PendingResult对象的finish()方法。
  • 如果onReceive方法中要执行耗时操作,可以将任务放到IntentService中执行。

    @Override
    public void onReceive(Context context, Intent intent) {
        // The task now runs on a worker thread.
        Intent intentService = new Intent(context, MyIntentService.class);
        context.startService(intentService);
    public class MyIntentService extends IntentService {
       @Override
       protected void onHandleIntent(@Nullable Intent intent) {
           BubbleSort.sort(data);
    

    或者,调用BroadcastReceiver告诉系统,我需要更多时间来处理消息。你处理完成之后,必须调用PendingResult对象的finish方法。

    final PendingResult pendingResult = goAsync();
    new AsyncTask<Integer[], Integer, Long>() {
       @Override
       protected Long doInBackground(Integer[]... params) {
           // This is a long-running operation
           BubbleSort.sort(params[0]);
           pendingResult.finish();