当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 {
@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 {
@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() {
@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