ANR官方文档

翻译的官方文章 原文链接

长时间阻塞Android应用的UI线程时会触发“Application Not Responding”(ANR)错误。如果应用处于前台,那么系统将会显示下图所示的ANR对话框。这个ANR对话框使得用户有机会去强制退出应用程序。


ANR对话框

Android应用的主线程就是负责更新应用界面的线程,ANR错误就是这个主线程无法处理用户输入事件或者更新界面导致的,从而导致不好的用户体验。想了解更多关于应用主线程信息,请查看官方文档Processes and Threads
如下几种情况会导致应用ANR:

  • 当应用界面处于前台时,应用在5秒内没有处理用户输入事件或者广播接收事件(BroadcastReceiver);
  • 当应用界面处于后台时,广播接受事件(BroadcastReceiver)在10秒内没有处理完成。

应用如果发生了ANR,我们可以按照这篇文章的指导来查找和修复问题。

检测和诊断ANR

Android 提供了一些工具让我们发现问题,并帮助我们诊断问题。如果应用已经发布了,那么当问题发生时Android vitals会提醒开发者,并且还有一些诊断工具帮助定位问题。

Android vitals

Android vitals的问题反馈能力可以帮助我们改善应用的性能。当应用频繁出现ANR时,Android vitals会反馈相关信息,反馈的信息在Play Console可以查看。一下情况会被认为是频繁ANR:

  • 一天里使用过应用的用户中有0.47%的用户出现了至少一次ANR;
  • 一天里使用过应用的用户中有0.24%的用户出现了两次及其以上的ANR。

想了解更多关于Google Play如果如何收集Android vitals数据信息的请查看Play Console文档

诊断ANR

诊断ANR时通常检查如下几项:

  1. 应用在主线程中执行了涉及I/O的耗时操作;
  2. 应用在主线程中执行耗时的计算;
  3. 应用在主线程调用了一个同步binder接口,另一个进程长时间没有返回;
  4. 主线程等待的锁长时间被其他线程持有;
  5. 主线程与其他线程形成死锁

下面的方法可以帮助我们分析出具体是哪种原因导致ANR。

Strict mode

在开发阶段使用StrictMode可以帮助我们找出意外在主线程执行了I/O操作的地方。我们可以在Application或者Activity层面使用StrictMode。

允许后台应用ANR显示对话框

只有在开发者选项中打开了“为后台应用显示ANR对话框”的开关,Android系统才会在应用长时间处理广播事件时显示ANR对话框。因为这个原因,后台引用的ANR对话框很可能不会显示,但实际APP任然存在性能问题。

Traceview

我们可以重复测试用例,使用Traceview来跟踪APP的运行情况,定位到主线程繁忙的地方。想了解更多关于Traceview的信息,请查看Profiling with Traceview and dmtracedump

获取trace文件

当发生ANR时Android系统会将trace信息保存到文件。在老的系统版本上所有ANR的trace信息是保存在/data/anr/traces.txt。在新的系统版本上trace信息是保存在多个如/data/anr/anr_*文件中。我们可以使用如下adb命令从设备或者模拟器中获取trace文件:

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

我们可以从物理设备上通过使用开发者选项中的提交错误报告选项获取错误报告,或者通过adb bugreport命令获取。详细信息查看 Capture and read bug reports

修复ANR

定位了问题以后可以使用下面的方法来修复问题。

主线程中的耗时代码

定位代码中耗时的地方。找打可以的复现用例,然后尝试复现ANR。如下图所示,Traceview的时间线展示了主线程耗时的地方。

耗时操作在主线程

从上图可以看出最耗时的代码是在onClick(View)中,即如下所示代码:

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

在这个例子中,我们应该把耗时操作从主线程移到工作线程中去。Android系统框架中有许多类可以帮助我们将耗时操作移到工作线程中区,详细参看 Helper classes for threading。下面的代码展示使用 AsyncTask来完成这个工作。

@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的截图,现在主线程可以响应用户输入事件了。
耗时操作在工作线程

IO在主线程

主线程耗时操作最常见的情况就是在主线程中执行了IO操作。推荐的做法是将IO操作移动到工作线程中,如上一部分所示。
IO常见的是网络请求和本地存储操作。更多信息可以参看 Performing network operationsSaving data

锁竞争

有时候导致ANR的代码不是主线程中。如果工作线程持有了主线程请求的锁,这就可能出现ANR。

耗时操作在工作线程中

上图所示大多数执行时间是在工作线程中。如果这种情况下还是出现了ANR,你应该在Android Device Monitor中查看主线程的状态。正常情况主线程是在 RUNNABLE 状态。如果主线程在 BLOCKED 状态,不能响应用户事件。如下Android Device Monitor截图。

主线程在Monitor的状态

下面的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
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]);
       }
   }
}

另一个例子是主线程一直等待工作线程结果的情况。

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

还有一些阻塞主线程的情况,包括使用 LockSemaphore,资源池(数据库连接池)或者其他互斥机制。
一般我们都会评估APP使用的锁资源。但我们想要必要ANR,就必须更加注意主线程使用到的锁资源。
确定锁持有的时间不要过长,甚至评估是否需要使用锁。如果你使用锁来判断根据工作线程的进度来跟新UI,那么可以使用如 onProgressUpdate()onPostExecute() 机制赖在工作线程和主线程间通信。

死锁

当A线程等待B线程所持有的锁1,而B线程却在等待A线程持有的锁2时就发生了死锁。这种情况ANR可能发生。死锁在计算机科学中已经研究的很充分了,我们可以使用死锁预防算法来避免死锁。更多信息可以参看 DeadlockDeadlock prevention algorithms

广播接收器中的耗时操作

APP能通过广播接收器处理广播消息,如飞行模式切换广播或者网络连接状态改变广播。当APP花了太多的时间处理广播消息时也会出现ANR。广播接收器可能有如下几种情况导致ANR:

  • 广播接收器在相当长的时间内没有完成 onReceive() 方法;
  • 广播接收器调用了 goAsync() 但是没有调用 PendingResult 对象的finish()方法。

APP在广播接收器的onReceive()方法中只应该做简单处理。如果确实需要执行复杂的逻辑,应该将任务发送到 IntentService 中去处理。
我们可以使用Traceview去检查广播接收器是否在主线程中执行了一个耗时操作。下图展示了在广播接收器中执行了大约100秒耗时操作的情况。

广播中的耗时操作

下图是一个在广播接收器中执行耗时操作的代码实例。

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

这种情况官方推荐将耗时操作移动到IntentService中去执行,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后耗时操作就从主线程转移到了工作线程了。下面是修改后Traceview的截图。


工作线程处理广播中的耗时操作

广播接收器可以使用 goAsync() 通知系统,当前广播事件需要更多的时间去处理。此外我们需要调用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信息可以参看 Keeping your App Resposive。更多关于线程的信息可以参看 Threading Performance

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容