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();
   }
}.execute(data);

但是使用goAsync()仍然可能导致ANR,你必须在10s之内完成操作

参考资料:https://developer.android.com/topic/performance/vitals/anr.html

点赞