安卓 - 我应该如何调查 ANR 问题?

181

有没有办法找出我的应用程序触发了ANR(应用程序无响应)的位置。我查看了/data目录下的traces.txt文件,发现了我的应用程序的跟踪信息。以下是跟踪信息的内容:

DALVIK THREADS:
"main" prio=5 tid=3 TIMED_WAIT
  | group="main" sCount=1 dsCount=0 s=0 obj=0x400143a8
  | sysTid=691 nice=0 sched=0/0 handle=-1091117924
  at java.lang.Object.wait(Native Method)
  - waiting on <0x1cd570> (a android.os.MessageQueue)
  at java.lang.Object.wait(Object.java:195)
  at android.os.MessageQueue.next(MessageQueue.java:144)
  at android.os.Looper.loop(Looper.java:110)
  at android.app.ActivityThread.main(ActivityThread.java:3742)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:515)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
  at dalvik.system.NativeStart.main(Native Method)

"Binder Thread #3" prio=5 tid=15 NATIVE
  | group="main" sCount=1 dsCount=0 s=0 obj=0x434e7758
  | sysTid=734 nice=0 sched=0/0 handle=1733632
  at dalvik.system.NativeStart.run(Native Method)

"Binder Thread #2" prio=5 tid=13 NATIVE
  | group="main" sCount=1 dsCount=0 s=0 obj=0x433af808
  | sysTid=696 nice=0 sched=0/0 handle=1369840
  at dalvik.system.NativeStart.run(Native Method)

"Binder Thread #1" prio=5 tid=11 NATIVE
  | group="main" sCount=1 dsCount=0 s=0 obj=0x433aca10
  | sysTid=695 nice=0 sched=0/0 handle=1367448
  at dalvik.system.NativeStart.run(Native Method)

"JDWP" daemon prio=5 tid=9 VMWAIT
  | group="system" sCount=1 dsCount=0 s=0 obj=0x433ac2a0
  | sysTid=694 nice=0 sched=0/0 handle=1367136
  at dalvik.system.NativeStart.run(Native Method)

"Signal Catcher" daemon prio=5 tid=7 RUNNABLE
  | group="system" sCount=0 dsCount=0 s=0 obj=0x433ac1e8
  | sysTid=693 nice=0 sched=0/0 handle=1366712
  at dalvik.system.NativeStart.run(Native Method)

"HeapWorker" daemon prio=5 tid=5 VMWAIT
  | group="system" sCount=1 dsCount=0 s=0 obj=0x4253ef88
  | sysTid=692 nice=0 sched=0/0 handle=1366472
  at dalvik.system.NativeStart.run(Native Method)

----- end 691 -----

我该如何找出问题的所在?跟踪中的方法都是SDK方法。


3
我有一个类似的报告,也发生在android.os.MessageQueue.nativePollOnce(Native Method)。我能安全地忽略它吗? - rds
13个回答

141
发生ANR时,某些长时间操作在“主”线程中进行。这是事件循环线程,如果它忙碌,Android无法处理应用程序中的任何其他GUI事件,因此会弹出ANR对话框。
现在,在您发布的跟踪中,主线程似乎正常,没有问题。它在MessageQueue中空闲,等待另一条消息进入。在您的情况下,ANR很可能是一个较长时间的操作,而不是永久阻塞线程的东西,因此在操作完成后,事件线程恢复了,您的跟踪在ANR之后继续进行。
要检测ANR发生的位置很容易,如果是永久性阻塞(例如死锁获取某些锁),但如果只是临时延迟则较困难。首先,检查您的代码,寻找易受攻击的地方和长时间运行的操作。示例可能包括使用套接字、锁、线程休眠和事件线程内的其他阻塞操作。您应该确保所有这些操作都在单独的线程中进行。如果没有发现问题,使用DDMS(Dalvik调试监视服务)并启用线程视图。这将显示您的应用程序中的所有线程,类似于您所看到的跟踪。重现ANR,并同时刷新主线程。这应该可以准确显示ANR发生时正在进行的操作。

7
唯一的问题就是“重现ANR” :-),你能否解释一下那个堆栈跟踪如何显示主线程处于'空闲状态',这将非常好。 - Blundell
26
堆栈跟踪显示主线程位于Looper(消息循环实现)中,并通过Object.wait进行定时等待。这意味着消息循环目前没有任何要分派的消息,并正在等待新消息到来。当系统意识到消息循环在处理一条消息时花费了太多时间,而没有处理队列中的其他消息时,就会发生ANR。如果循环正在等待消息,显然这种情况并没有发生。 - sooniln
3
你好,@Soonil。你知道“剩余部分”(rest of the sections)的含义吗?例如“Binder thread 3”、“Binder thread 2 JDWP demon prio 5”。sCount、dsCount、obj、sysTid、nice sched 含义是什么?此外,它还包含了 VMWAIT、RUNNABLE 和 NATIVE 等信息。 - minhaz
1
我的应用程序是基于NDK的,我遇到了相同的ANR问题。同时,主线程没有问题。我尝试使用DDMS,在工作线程冻结时刷新它。不幸的是,我只得到了一行NativeStart :: run。DDMS线程视图是否能够检查本地NDK线程?此外:StrictMode没有发现任何问题。 - Bram
我也对 sCount=1 dsCount=0 obj=0x40eb94d0 self=0x1def710 sysTid=3596 nice=0 sched=0/0 cgrp=default handle=1074915540 schedstat=( 2384227922 1199801851 4803 ) utm=192 stm=46 core=0 感兴趣。 - landry
7
请参见 http://elliotth.blogspot.com/2012/08/how-to-read-dalvik-sigquit-output.html 以获得有关输出信息的详细解释。 - sooniln

112

你可以在API 9及以上版本中启用StrictMode

StrictMode最常用于捕获应用程序主线程上的意外磁盘或网络访问,这是接收UI操作和进行动画的地方。通过保持应用程序的主线程响应性,您还可以防止用户看到ANR对话框

public void onCreate() {
    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                           .detectAll()
                           .penaltyLog()
                           .penaltyDeath()
                           .build());
    super.onCreate();
}

使用penaltyLog()函数,您可以在使用应用程序时观察adb logcat的输出,以便及时发现违规情况。


31
小贴士-使用if(BuildConfig.DEBUG)...来防止在生产中被包含。 - Amir Uval
@uval 你说的“防止生产中包含”是什么意思?!! - Muhammed Refaat
2
@MuhammedRefaat 它并不能防止任何ANR。它会立即崩溃应用程序,而不是在5秒后崩溃。例如,如果您在主线程上访问数据库并且需要2秒钟,您将不会收到ANR,但StrictMode将使应用程序崩溃。StrictMode严格用于调试阶段,而不是生产阶段。 - Amir Uval
@uval 如果我不介意应用程序崩溃怎么办?http://stackoverflow.com/questions/29392969/using-strictmode-in-app-production-phase?noredirect=1#comment46965864_29392969 - Muhammed Refaat
1
@MuhammedRefaat 将我的回复添加到了你的问题中。 - Amir Uval
显示剩余7条评论

89
你想知道哪个任务占用了UI线程。跟踪文件可以帮助你找到任务,你需要调查每个线程的状态。

线程的状态

  • running - 执行应用程序代码
  • sleeping - 调用Thread.sleep()
  • monitor - 等待获取监视器锁定
  • wait - 在Object.wait()中
  • native - 执行本地代码
  • vmwait - 等待VM资源
  • zombie - 线程正在死亡过程中
  • init - 线程正在初始化(你不应该看到这个)
  • starting - 线程即将启动(你也不应该看到这个)

关注SUSPENDED和MONITOR状态。监视器状态指示要调查的线程,线程的暂停状态可能是死锁的主要原因。

基本调查步骤

  1. 查找“等待锁定”
    • 你可以找到监视器状态"Binder Thread #15" prio=5 tid=75 MONITOR
    • 如果找到“等待锁定”,你就很幸运了
    • 例如:waiting to lock <0xblahblah> (a com.foo.A) held by threadid=74
  2. 你会注意到“tid=74”现在持有一个任务。所以去找tid=74
  3. tid=74可能是SUSPENDED状态!找到主要原因!

跟踪文件并不总包含“等待锁定”。在这种情况下,很难找到主要原因。


1
很好的解释。现在我更容易理解ANR日志了。但是我仍然有一个问题,就是在步骤1中我可以轻松地找到线程ID,但是当我在步骤2中尝试去查找它所在的位置并检查其状态时,我无法找到它。有什么想法如何继续处理它? - THZ
2
我在“HeapTaskDaemon”守护进程中看到了“- waiting to lock an unknown object”,线程优先级为5,tid为8。这是什么意思?有人可以帮忙解释一下吗? - Hilal
这个回复解决了2023年的问题。 :) - dextertron_

20

我已经学习Android几个月了,虽然还不是专家,但是我对ANR文档感到非常失望。

大多数建议都是为了避免或通过盲目查看代码来修复它们,这很好,但我找不到关于分析跟踪的任何内容。

有三件事你需要仔细观察ANR日志:

1) 死锁: 当一个线程处于WAIT状态时,可以查看详细信息找出它正在"heldby="谁。大多数情况下,它会被自己持有,但如果它被另一个线程持有,那就可能是一个危险信号。去查看那个线程并查看它持有的内容。你可能会发现一个循环,这是明确的指示出问题了。这种情况非常罕见,但是第一点因为当它发生时,会是噩梦般的体验

2) 主线程等待: 如果你的主线程处于WAIT状态,请检查它是否被另一个线程持有。这不应该发生,因为你的UI线程不应该被后台线程持有。

这两种情况都意味着你需要显著地重新编写你的代码。

3) 主线程上的重操作: 这是ANRs最常见的原因,但有时很难找到和修复。查看主线程详细信息。滚动下堆栈跟踪,直到你看到你认识的类(来自你的应用程序)。查看跟踪中的方法并确定是否在这些位置进行网络调用,数据库调用等。

最后,我很抱歉自推销我的代码,但您可以使用我编写的Python日志分析器。请访问https://github.com/HarshEvilGeek/Android-Log-Analyzer。该分析器可以处理日志文件、打开ANR文件、寻找死锁、寻找等待主线程、查找代理日志中未捕获的异常,并将结果以相对易读的方式打印在屏幕上。阅读ReadMe文件(即将添加)以了解如何使用它。在过去的一周里,它帮助了我很多!


7

5
无论何时分析时间问题,调试通常无法帮助解决问题,因为在断点处冻结应用程序会使问题消失。
最好的方法是在应用程序的不同线程和回调中插入大量的日志记录调用(Log.XXX()),并查看延迟发生的位置。如果需要堆栈跟踪,请创建一个新的异常(只需实例化一个)并记录它。

3
感谢关于在需要堆栈跟踪时创建新异常的建议。在调试时非常有帮助 :) - kuchi

3

什么会触发ANR?

通常,如果一个应用无法响应用户输入,系统就会显示ANR。

在任何可能执行较长时间操作的情况下,您都不应该在UI线程中执行这些操作,而是应该创建一个工作线程,在那里执行大部分工作。这可以保持UI线程(驱动用户界面事件循环)运行,并防止系统认为您的代码已经冻结。

如何避免ANR?

Android应用程序通常默认完全在单个线程上运行,即“UI线程”或“主线程”。这意味着如果您的应用程序在UI线程中执行需要较长时间才能完成的操作,它可能会触发ANR对话框,因为您的应用程序没有机会处理输入事件或意图广播。

因此,任何在UI线程中运行的方法都应尽可能少地使用该线程。特别是活动应该在关键生命周期方法(例如onCreate()和onResume())中尽可能少地进行设置。可能需要较长时间才能完成的操作,例如网络或数据库操作,或计算量比较大的操作,例如调整位图大小,应该在工作线程中执行(或在数据库操作的情况下通过异步请求执行)。

代码:使用AsyncTask类的工作线程

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    // Do the long-running work in here
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }

    // This is called each time you call publishProgress()
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    // This is called when doInBackground() is finished
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

代码:执行工作线程

要执行此工作线程,只需创建一个实例并调用execute():

new DownloadFilesTask().execute(url1, url2, url3);

Source

http://developer.android.com/training/articles/perf-anr.html


1
基于 @Horyun Lee 的回答,我写了一个小的 Python 脚本 来帮助调查来自 traces.txt 的 ANR。如果您已在系统上安装了 graphviz,则可以将 ANR 输出为图形。
$ ./anr.py --format png ./traces.txt

如果在文件 traces.txt 中检测到ANR,则会输出如下的png文件。这更加直观易懂。

enter image description here

上面使用的示例traces.txt文件是从这里获取的。

1

不确定这是否有帮助。我的问题是应用程序会在安卓10的设备上崩溃和冻结,然后强制重启,但在安卓6上运行良好,logcat中没有任何显示。崩溃很难复现,非常不可预测。

我花了将近两周时间搜索和解决ANR问题,但都无济于事。最终同步gradle解决了所有问题...... 新手错误。

希望这能帮助到某些人。


1
考虑使用ANR-Watchdog库来准确地跟踪和捕获ANR堆栈跟踪的详细信息。然后可以将它们发送给崩溃报告库。在这种情况下,建议使用setReportMainThreadOnly()。您可以让应用程序在冻结点抛出非致命异常,也可以在ANR发生时强制退出应用程序。

请注意,发送到Google Play开发者控制台的标准ANR报告通常不足以精确定位问题所在。因此需要第三方库的帮助。


请问您能否详细说明一下,当主线程发生ANR时,我们如何共享所有线程的堆栈跟踪信息? - Shubham AgaRwal
1
@Killer: setReportMainThreadOnly() 主要用于使用 Crashlytics 时,因为 Crashlytics 无法正确显示所有线程的大量数据。如果您想记录所有线程的数据,则可以将整个 ANRError 捕获到文件中或将其打印到 Logcat 中。如果您还想将此数据发送到 Crashlytics,则应首先对数据进行修剪,或尝试类似于 anrError.getCause()anrError.getMessage() 的方法,以将其减少到仅主线程。更多信息:https://github.com/SalomonBrys/ANR-WatchDog/issues/29#issuecomment-339480254 - Mr-IDE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接