Android 性能优化之ANR
和你一起终身学习,这里是程序员 Android
本篇文章主要介绍 Android 开发中的 ANR 部分知识点,通过阅读本篇文章,您将收获以下内容:
1.ANR 是什么
2.检测和诊断问题
3.诊断ANR
4.解决ANR问题
1.ANR 是什么
ANR 是Application Not Responding的简称。
当Android应用程序的UI线程被阻止时间太长时,将触发“应用程序无响应”(ANR)错误。如果应用程序位于前台,则系统会向用户显示一个对话框,如图1所示。ANR对话框使用户有机会强制退出该应用程序。
图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时,需要寻找一些常见的模式:
- 该应用程序正在执行涉及主线程上I / O的慢速操作。
- 该应用程序正在主线程上进行长时间的计算。
- 主线程正在对另一个进程进行同步联编程序调用,而另一个进程要花很长时间才能返回。
- 主线程被阻塞,等待另一个线程上正在进行的长时间操作的同步块。
- 主线程与另一个线程处于死锁状态,无论是在您的进程中还是通过绑定程序调用。主线程不仅等待长时间的操作完成,而且处于死锁状态。有关更多信息,请参见 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秒钟以上。
图2.Traceview时间线显示繁忙的主线程
图2向我们展示了大多数有问题的代码都发生在onClick(View)处理程序中,如以下代码示例所示@Overridepublic 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)助手类来处理工作线程上的任务:
@Overridepublic 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所示。主线程可用于响应用户事件。
图3. Traceview时间线显示了工作线程处理的工作
主线程IO操作
在主线程上执行IO操作是导致主线程执行缓慢操作的常见原因,这可能会导致ANR。建议将所有IO操作移至工作线程,如上一节中所示。
IO操作的一些示例是网络和存储操作。有关更多信息,请参阅执行网络操作和保存数据。
争夺持有对象锁
在某些情况下,导致ANR的工作不会直接在应用程序的主线程上执行。如果辅助线程对主线程完成其工作所需的资源持有锁,则可能会发生ANR。
例如,图4显示了Traceview时间线,其中大部分工作是在工作线程上执行的
图4. Traceview时间线,显示了正在工作线程上执行的工作
但是,如果您的用户仍在遇到ANR,则应查看Android Device Monitor中主线程的状态。通常,如果主线程已RUNNABLE准备好更新UI??,并且处于响应状态,则主线程处于工作状态。
但是,如果主线程无法恢复执行,则它处于BLOCKED状态且无法响应事件。状态在Android设备监视器上显示为Monitor或Wait,如图5所示。
图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)
...
查看跟踪可以帮助您找到阻塞主线程的代码。以下代码负责在上一个跟踪中持有阻塞主线程的锁:
@Overridepublic 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。
- 您的应用仅应使用的onReceive()方法执行简短的操作BroadcastReceiver。但是,如果您的应用由于广播消息而需要更复杂的处理,则应将任务推迟到 IntentService。
- 您可以使用Traceview之类的工具来确定您的广播接收器是否在应用程序的主线程上执行了长时间运行的操作。例如,图6显示了广播接收器的时间轴,该接收器在主线程上处理消息大约100秒。
- 图6. Traceview时间线显示了BroadcastReceiver在主线程上的工作
- 这种行为可能是由于对的onReceive()方法执行了长时间运行的操作引起的BroadcastReceiver,如以下示例所示:
@Overridepublic void onReceive(Context context, Intent intent) {
// This is a long-running operation
BubbleSort.sort(data);
}
在这种情况下,建议将长时间运行的操作移至,IntentService因为它使用辅助线程执行工作。以下代码显示了如何使用IntentService来处理长时间运行的操作:
@Overridepublic 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时间轴中推迟到工作线程的工作
图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的更多信息,请参阅 使应用程序保持响应状态。有关线程的更多信息,请参见 线程性能。