性能优化系列之ANRs

和你一起终身学习,这里是程序员Android

本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容:

本篇文章主要介绍 Android 开发中的 ANR 部分知识点,通过阅读本篇文章,您将收获以下内容:

1.ANR 是什么

1.ANR 是什么

ANR 是Application Not Responding的简称。

当Android应用程序的UI线程被阻止时间太长时,将触发“应用程序无响应”(ANR)错误。如果应用程序位于前台,则系统会向用户显示一个对话框,如图1所示。ANR对话框使用户有机会强制退出该应用程序。

640?wx_fmt=other

图1.向用户显示的ANR对话框

ANR是一个问题,因为负责更新UI的应用程序主线程无法处理用户输入事件或绘制,从而使用户感到沮丧。有关应用主线程的更多信息,请参阅进程和线程。

发生以下情况之一时,将为您的应用触发ANR:

  • 当您的活动处于前台时,您的应用BroadcastReceiver在5秒钟内未响应输入事件或(例如按键或屏幕触摸事件)。

  • 虽然前台没有活动,但是您BroadcastReceiver还没有在相当长的时间内完成执行。

如果您的应用遇到ANR,则可以使用本文中的指导来诊断和解决问题。

2. 检测和诊断问题

Android提供了多种方法来让您知道您的应用存在问题,并帮助您进行诊断。如果您已经发布了应用程序,则Android vitals可以提醒您发生问题,并且有诊断工具可以帮助您发现问题。

Android vitals

当您的应用程序展示出过多的ANR时 ,Android生命体可以通过Play控制台提醒您,从而提高应用程序的性能 。在以下情况下,Android vitals认为ANR过多:

  • 在每天至少0.47%的时间里展示至少一种ANR。

  • 在每天至少0.24%的时间内展示2个或更多ANR。

一个日常的会话是指在使用你的应用程序的日子。

有关Google Play如何收集Android生命数据的信息,请参阅 Play控制台文档。

3.诊断ANR

诊断ANR时,需要寻找一些常见的模式:

  1. 该应用程序正在执行涉及主线程上I / O的慢速操作。

  2. 该应用程序正在主线程上进行长时间的计算。

  3. 主线程正在对另一个进程进行同步调用,而另一个进程要花很长时间才能返回。

  4. 主线程被阻塞,等待另一个线程上正在进行的长时间操作的同步块。

  5. 主线程与另一个线程处于死锁状态,无论是在您的进程中还是通过绑定程序调用。主线程不仅等待长时间的操作完成,而且处于死锁状态。有关更多信息,请参见 Wikipedia上的死锁。

以下技术可以帮助您找出导致ANR的原因是哪些。

StrictMode

使用StrictMode有助于您在开发应用程序时在主线程上查找意外的I / O操作。您可以StrictMode在应用程序或活动级别使用。

启用后台ANR 弹窗

仅当在设备的“ 开发人员”选项中启用了“ 显示所有ANR”时,Android才会为需要花费太长时间才能处理广播消息的应用程序显示ANR对话框。因此,并非总是向用户显示后台ANR对话框,但是该应用仍可能遇到性能问题。

使用TraceView 跟踪

您可以在查看用例时使用Traceview跟踪正在运行的应用程序,并确定主线程繁忙的位置。有关如何使用Traceview的信息,请参见使用Traceview和dmtracedump进行概要分析。

分析手机内部traces 文件

当遇到ANR时,Android会存储跟踪信息。在较旧的OS版本/data/anr/traces.txt上,设备上只有一个文件。在较新的OS版本中,存在多个/data/anr/anr_*文件。您可以通过使用Android调试桥(adb)作为root用户从设备或仿真器访问ANR跟踪 :

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

您可以使用设备上的“获取错误报告开发者”选项或开发计算机上的adb bugreport命令从物理设备捕获错误报告。有关更多信息,请参阅捕获和阅读错误报告。

4.解决ANR 问题

确定问题之后,可以使用本节中的提示来修复常见问题。

主线程 进行耗时操作

确定代码中应用主线程忙于5秒钟以上的位置。在您的应用中查找可疑的用例,然后尝试重现ANR。

例如,图2显示了Traceview时间线,其中主线程耗时大于5秒钟以上。

640?wx_fmt=other

图2.Traceview时间线显示繁忙的主线程

图2向我们展示了大多数有问题的代码都发生在onClick(View)处理程序中,如以下代码示例所示

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

在这种情况下,应将在主线程中运行的工作移至工作线程。Android Framework包含可帮助将任务移至工作线程的类,有关更多信息,请参阅线程的帮助程序类。以下代码显示了如何使用[AsyncTask](https://developer.android.google.cn/reference/android/os/AsyncTask.html)助手类来处理工作线程上的任务:

@Override
public void onClick(View view) {
   // The long-running operation is run on a worker thread
   new AsyncTask<Integer[], Integer, Long>() {
       @Override
       protected Long doInBackground(Integer[]... params) {
           BubbleSort.sort(params[0]);
       }
   }.execute(data);
}

Traceview显示大多数代码都在工作线程上运行,如图3所示。主线程可用于响应用户事件。

640?wx_fmt=other

图3. Traceview时间线显示了工作线程处理的工作

主线程IO操作

在主线程上执行IO操作是导致主线程执行缓慢操作的常见原因,这可能会导致ANR。建议将所有IO操作移至工作线程,如上一节中所示。

IO操作的一些示例是网络和存储操作。有关更多信息,请参阅执行网络操作和保存数据。

争夺持有对象锁

在某些情况下,导致ANR的工作不会直接在应用程序的主线程上执行。如果辅助线程对主线程完成其工作所需的资源持有锁,则可能会发生ANR。

例如,图4显示了Traceview时间线,其中大部分工作是在工作线程上执行的

640?wx_fmt=other

图4. Traceview时间线,显示了正在工作线程上执行的工作

但是,如果您的用户仍在遇到ANR,则应查看Android Device Monitor中主线程的状态。通常,如果主线程已RUNNABLE准备好更新UI,并且处于响应状态,则主线程处于工作状态。MonitorWait,如图5所示。

640?wx_fmt=other

图5. Monitor状态下的主线程

以下跟踪显示了应用程序的主线程在等待资源时被阻止:

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

查看跟踪可以帮助您找到阻塞主线程的代码。以下代码负责在上一个跟踪中持有阻塞主线程的锁:

@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]);
       }
   }
}

另一个示例是应用程序的主线程正在等待工作线程的结果,如以下代码所示。请注意,在Kotlin中,建议不要使用wait()和 notify()模式,因为Kotlin具有处理并发的机制。使用Kotlin时,如果可能,应使用Kotlin特定的机制。

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

还有其他一些情况可能会阻塞主线程,包括使用Lock的线程Semaphore以及资源池(例如数据库连接池)或其他互斥(mutex)机制。

通常,应该评估应用程序对资源的锁定,但是如果要避免使用ANR,则应查看对主线程所需的资源所持有的锁定。

确保将锁保持的时间最少,甚至更好,请评估应用程序是否首先需要保持。如果要使用锁根据工作线程的处理确定何时更新UI,请使用诸如onProgressUpdate()和的机制 onPostExecute() 在工作线程和主线程之间进行通信。

死锁

当线程进入等待状态时会发生死锁,因为另一个线程拥有所需的资源,而另一个线程也在等待第一个线程拥有的资源。如果应用程序的主线程处于这种情况下,则可能会发生ANR。

死锁是计算机科学中经过充分研究的现象,并且可以使用死锁预防算法来避免死锁。

有关更多信息,请参见Wikipedia上的死锁和 死锁预防算法。

广播接收慢

应用程序可以通过广播接收器响应广播消息,例如启用或禁用飞行模式或连接状态更改。当应用花费太长时间来处理广播消息时,就会发生ANR。

在以下情况下会发生ANR:

  • 广播接收器onReceive()在相当长的时间内还没有完成执行其方法。

  • 广播接收器调用该对象goAsync(),但未能调用。finish()PendingResult。

方法执行简短的操作BroadcastReceiver。但是,如果您的应用由于广播消息而需要更复杂的处理,则应将任务推迟到 IntentService。

640?wx_fmt=other

图6. Traceview时间线显示了BroadcastReceiver在主线程上的工作

方法执行了长时间运行的操作引起的BroadcastReceiver,如以下示例所示:

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

在这种情况下,建议将长时间运行的操作移至,IntentService因为它使用辅助线程执行工作。以下代码显示了如何使用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);
   }
}

使用的结果是IntentService,长时间运行的操作在工作线程而不是主线程上执行。图7显示了在Traceview时间轴中推迟到工作线程的工作

640?wx_fmt=other

图7. Traceview时间线显示了在工作线程上处理的广播消息

您的广播接收器可以用来goAsync() 向系统发送信号,通知它需要更多时间来处理消息。但是,您应该调用 finish() 该 PendingResult 对象。下面的示例演示如何调用finish()以使系统回收广播接收器并避免ANR:

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。ANR超时仍然适用。

有关ANR的更多信息,请参阅 使应用程序保持响应状态。有关线程的更多信息,请参见 线程性能。

友情推荐

Android开发干货分享

至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

640?wx_fmt=png

posted @ 2019-10-18 08:00  程序员Android的博客  阅读(213)  评论(0编辑  收藏  举报