一. ANR简介
- 描述:UI线程如果停止响应太长的时间, “Application Not Responding” (ANR) 就被触发。
- 如果被阻塞的app处于前台,系统会显示一个ANR对话框。
- 触发:以下两个条件,任意各一个都会导致ANR
- 当app处于前台时,在5s内无法相应用户输入或广播。
- 当app没有activity处于前台时,广播接收器正在进行长时间的任务,且无法结束。
二. 检测和诊断ANR
- 使用Android vitals。Play Console会在vital dashboard中显示ANR数据。
- 只要满足以下条件,Play Console会在vitals会认为 ANRs 过多:
- 最少每天session的0.47%出现至少一次ANR
- 最少每天session的0.24%出现2次或以上ANR
- 每天session是指每天App的使用次数
- 诊断ANR,下面将列举一些典型的ANR原因
- 在主线程进行很慢的I/O操作
- 在主线程进行长时间的计算
- 在主线程进行同步的binder远程调用,而远程调用需要很长的事件才能返回
- 主线程阻塞,并等待其它线程执行长时间操作的同步块。
- 通过自己进程或远程方法调用,主线程被其它线程死锁。此时,主线程并非在等待长时间操作完成,而是处于死锁状态。
- 通过以下方法来查找发生ANR的具体原因
- 使用严格模式
Strict mode
,可以找到主线程中意外的I/O操作。可以在Application或Activity中使用。一般推荐在Application级就使用。 - 在开发者模式中,选择打开”显示所有ANR对话框”。默认情况下这个开关是关闭的,所有有时后台app发生了ANR,但不会显示ANR对话框。
- 使用Traceview,可以追踪app的运行轨迹,并确定主线程被卡住的地方。
- 在root情况下,可以从设备拉取traces文件
- 老版本中,文件位于/data/anr/traces.txt
- 较新的版本中,变成了多个文件/data/anr/anr_*
- 通过
adb bugreport
获取bug reports,或者从设备的开发者选项中获取。
三. 解决问题
- 耗时计算:确定主线程中超过5秒操作的可疑代码,尝试重现ANR。Traceview timeline 可以直观的显示很忙的主线程操作。
- 确定长耗时的操作后,将这些操作以异步方式执行。可以使用
AsyncTask
、Thread
、线程池等方式来实现,保证主线程的交互响应。 - Traceview同样可以显示工作线程正在处理的任务
- 示例代码,修改前:
@Override
public void onClick(View view) {
// 耗时很长的冒泡排序
BubbleSort.sort(data);
}
- 示例代码,修改后:
@Override
public void onClick(View view) {
// 将长耗时任务放在工作线程(子线程)中
new AsyncTask<Integer[], Integer, Long>() {
@Override
protected Long doInBackground(Integer[]... params) {
BubbleSort.sort(params[0]);
}
}.execute(data);
}
- IO操作:推荐将IO操作放到子线程中,就像上一条一样。
- 锁竞争:如果子线程对某资源加锁,而主线程同样需要这个资源,ANR就可能会发生。
- 锁竞争不一定会导致ANR。
- 由于锁竞争导致ANR发生时,主线程将处于阻塞状态。观察Traceview,可以发现主线的状态为
Monitor
或Wait
。 - 查看trace file示例,也显示了资源锁竞争的问题:
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.MainActivityLockTask.doInBackground(MainActivity.java:135)atandroid.os.AsyncTask
L
o
c
k
T
a
s
k
.
d
o
I
n
B
a
c
k
g
r
o
u
n
d
(
M
a
i
n
A
c
t
i
v
i
t
y
.
j
a
v
a
:
135
)
a
t
a
n
d
r
o
i
d
.
o
s
.
A
s
y
n
c
T
a
s
k
2.call(AsyncTask.java:305)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTaskSerialExecutor
S
e
r
i
a
l
E
x
e
c
u
t
o
r
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) {
// 子线程在lockedResource上持有锁
new LockTask().execute(data);
synchronized (lockedResource) {
// 主线程在这里需求lockedResource
// 但必须等待子线程结束...
}
}
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]);
}
}
}
```
- 示例代码,修改后:
```
@Override
public void onClick(View v) {
WaitTask waitTask = new WaitTask();
synchronized (waitTask) {
try {
waitTask.execute(data);
// 等待子线程的通知
waitTask.wait();
} catch (InterruptedException e) {}
}
}
class WaitTask extends AsyncTask<Integer[], Integer, Long>{
@Override
protected Long doInBackground(Integer[]... params) {
synchronized (this) {
BubbleSort.sort(params[0]);
// 完成,通知主线程
notify();
}
}
}
```
- 除了锁,还有其它的情况会导致主线程block。比如:
-
Semaphore
信号标 - 资源池,比如数据库连接池
- 其它互斥机制
- 为了避免锁竞争导致的ANR,应当:
- 仔细检查主线程中对锁的请求
- 尽量缩短子线程持有锁的时间
- 评估是否需要持有锁。如果真的需要,应当使用合理的线程通信方法。
- Deadlocks
- 当两个线程互相等待对方持有的资源,导致都进入等待状态,就发生了死锁。
- 如果UI线程和其它线程处于了死锁状态,很可能会发生ANR。
- 请使用各种预防死锁算法来解决问题。
- 慢广播:如果花费太长的时间用来处理广播消息时,会出现ANR。
- 以下两种情况会导致ANR:
-
onReceive()
执行时间太长。So不要在这个方法中执行耗时操作。 - 广播接收器调用
goAsync()
,但没有调用PendingResult#finish()
- 如果需要对广播消息进行更复杂的处理,应当推迟到
IntentService
中去执行。 - 可以通过Traceview 来追踪耗时操作。
onReceive()
示例代码,修改前:
@Override
public void onReceive(Context context, Intent intent) {
// 长耗时
BubbleSort.sort(data);
}
onReceive()
示例代码,修改后:
@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);
}
}
- 广播接收器可以通过
goAsync
给系统发送一个请求更多时间来处理消息的信号,但在处理完成后,应该调用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);
请注意,只能在主线程中使用。如果将这段代码放到子线程中,ANR问题仍会出现。