当主线程被阻塞太长时间时,会触发ANR(Application Not Response)。如果App正在前台运行,系统将向用户展示ANR弹框如下。ANR弹框让用户可以强制退出应用。
ANR是指:因为应用主线程,也就是负责更新界面的线程,不能处理用户输入事件或者绘制,给用户造成困扰。ANR可能会在如下情况下出现:
- 当activity处于前台时,应用在5s内不能完成输入或者广播事件
- 当activity不处于前台时,广播处理事件在一段时间内不能完成
如果您的应用正经历ANRs,您可以使用下列指南去诊断并修复此类问题
检测和诊断ANR
Android提供了几种方式让你了解到您的应用存在问题并帮助您诊断。如果您的应用已经线上发布,Android vitals可能警示您可能出现了问题,并附有诊断工具助您解决问题
Android vitals(Bugly与此类似)
不介绍
诊断ANR(ANR发生在哪儿)
在诊断ANR时,有一些常见常见需要注意
- 应用主线程中执行I/O耗时操作
- 应用主线程中执行长时间运算
- 应用主线程正执行同步binder调起另外一个进程,该进程返回时间太长
- 应用主线程被同步锁锁住,锁此时正被其他线程占用
- 应用主线程和其他线程出现死锁,可能时自己应用的线程,也可能是其他应用线程
下面几项技术可以帮助你找到导致ANR的原因:
严格模式
使用严格模式可以帮你找到主线程中偶然使用到的I/O操作,你可以在Activity或者Application级别启动严格模式StrictMode
开启后台ANR弹框
Android仅在后台ANR弹框开关开启是才会为那些需要很长时间来处理广播消息的应用程序显示ANR对话框。正式由于这个原因,后台anr弹框不总是对用户展示,但是这样不显示应用性能问题还是得不到解决。
TraceView
你可以使用Traceview来获得运行应用程序的trace,同时通过用例来识别主线程繁忙的地方。关于如何使用traceView,可以点击Profiling with Traceview and dmtracedump
导出traces file
当发生ANR时候,Android会存储一份trace信息在设备中。老设备存储地址为:/data/anr/traces.txt,新设备存储地址/data/anr/anr_*,可以使用adb导出
adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>
您可以通过通过勾选设置设置中的bug上报,或者adb 命令来获取bug report。更多信息请阅读Capture and read bug reports
解决这些问题
当你定位到这些问题后,可以通过此模块提供的一些贴士修复一些常见问题
主线程代码运行慢
识别代码中应用程序主线程繁忙超过5秒的地方。在你的应用程序中寻找可疑的用例,并尝试重现ANR。
例如,下图显示了一个Traceview时间轴,其中主线程繁忙超过5秒
上图向我们展示了onclick中事件点击处理超过5s。在这种情况下,您应该将主线程中的任务移到子线程中去执行。android framework中有这样的class帮您将任务抛到子线程中执行,详细信息请查询helper class for threading。下面的代码展示了如何使用asyncTask将任务移动至子线程中执行:
override fun onClick(v: View) {
// The long-running operation is run on a worker thread
object : AsyncTask<Array<Int>, Int, Long>() {
override fun doInBackground(vararg params: Array<Int>): Long? =
BubbleSort.sort(params[0])
}.execute(data)
}
Traceview显示大部分代码在工作线程上运行,如图3所示。主线程可用来响应用户事件。
主线程执行IO操作
主线程执行IO操作也是一种常见的导致主线程运行慢的场景,并最终导致ANR。和前文描述的一样,这种情况下也推荐将IO操作移动至子线程中执行
一些常见的IO操作包括网络请求和存储操作,获取更多信息,请查阅performing network和saving data
锁竞争
在某些场景下,并不是由于主线程工作任务导致ANR。如果一个子线程持有一个资源锁,而主线程恰好需要获取此资源锁以便完成当前任务,这种场景就可能出现ANR。
下图4展示了traceview时间轴,其中大多素工作运行在工作线程
但是,如果您的app正发生anr时,您应该通过android device monitor查看主线程状态,通常情况下,如果主线程已经准备好更新UI而且是可以响应事件时,它会处于RUNNABLE状态。但是如果主线程不能继续执行,那么它就处于阻塞状态,不能响应事件。状态在monitor上显示为Monitor或Wait,如图5所示
从下面trace可以看出主线程在等待资源时被阻塞
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)
通过trace可以定位到导致主线程阻塞的代码,下面的这段代码展示的是持有锁导致上述trace中主线程阻塞
override fun onClick(v: View) {
// The worker thread holds a lock on lockedResource
LockTask().execute(data)
synchronized(lockedResource) {
// The main thread requires lockedResource here
// but it has to wait until LockTask finishes using it.
}
}
class LockTask : AsyncTask<Array<Int>, Int, Long>() {
override fun doInBackground(vararg params: Array<Int>): Long? =
synchronized(lockedResource) {
// This is a long-running operation, which makes
// the lock last for a long time
BubbleSort.sort(params[0])
}
}
另外一个例子是主线程在等待子线程返回值。值得注意的是:不推荐在kolin中使用wait()和notify(),kolin对应并发有自己的机制
fun onClick(v: View) {
val lock = java.lang.Object()
val waitTask = WaitTask(lock)
synchronized(lock) {
try {
waitTask.execute(data)
// Wait for this worker thread’s notification
lock.wait()
} catch (e: InterruptedException) {
}
}
}
internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
override fun doInBackground(vararg params: Array<Int>): Long? {
synchronized(lock) {
BubbleSort.sort(params[0])
// Finished, notify the main thread
lock.notify()
}
}
}
另外,还有其他不同的场景可导致主线程被阻塞,包括锁,信号量,资源池(如数据库连接资源池)或者其他互斥机制。
通常,您应该评估应用程序对资源持有的锁,但是如果您想避免ANRs,那么您应该查看主线程所需资源持有的锁。
确保锁被占用的时间最少,或者更好的是,首先评估应用程序是否需要占用锁。如果您使用锁来根据工作线程的处理情况来决定何时更新UI,那么可以使用诸如onProgressUpdate()和onPostExecute()之类的机制来在工作线程和主线程之间进行通信
死锁
当一个线程进入等待状态时而所需的资源由另一个线程持有,该线程也在等待第一个线程持有的资源,此时就会发生死锁。如果应用程序的主线程在这种情况下,anr很可能会发生。
广播接收器中代码运行慢
应用程序可以通过广播接收器响应广播消息,比如启用或禁用飞机模式或改变连接状态。当应用程序处理广播消息的时间过长时,就会发生ANR。
ANR会发生在以下场景:
- 广播接收器在相当长的时间内没有完成其onReceive()方法的执行。
- 广播接收器调用goAsync(),但未能调用PendingResult对象上的finish()
您的应用在onReceive中只能执行简短操作。但是如果需要在广播接收器中执行更加复杂的操作,您应当通过IntentService完成任务。
您可以使用工具如traceview去鉴定是否您的广播接收器中是否存在主线程长时操作。图6的时间轴显示了广播接收器中主线程处理消息时长大约100s
上面的场景可能是由于onreceive()中耗时导致,如下
override fun 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,长时间运行的操作是在工作线程而不是主线程上执行的。图7Traceview时间轴可以看出工作抛到了工作线程中执行。
您的广播接收器可以使用goAsync()向系统发出信号,表明它需要更多的时间来处理消息。但是,您应该对PendingResult对象调用finish()。下面的例子展示了如何调用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信息,请阅读keeping your app responsive,想要获取更多线程信息,请阅读threading