我们可以理解 Android 崩溃日志吗?让我们找出答案

Android 崩溃日志错误:解释

Android 上的崩溃可能会让用户非常沮丧,以至于在经历了两次崩溃之后,典型的用户就会卸载你的应用程序。幸运的是,Android 框架为调试崩溃提供了一些很棒的工具,并提供了一些有用的崩溃日志,开发人员可以阅读这些日志以确定导致该严重问题的原因。在这篇博文中,我们将介绍系统使用的三个最重要的崩溃日志:异常堆栈跟踪、ANR 跟踪和 NDK 墓碑。

异常堆栈跟踪

JVM 堆栈跟踪是典型 Android 应用程序会遇到的最常见的崩溃类型,因为大多数应用程序都是用 Kotlin 或 Java 编写的。在 JVM 语言中,异常情况下会引发异常,并包含有关出错的错误条件的调试信息,例如带有文件/行号信息的堆栈跟踪和错误消息。

如果应用程序没有安装崩溃报告 SDK,那么检索崩溃日志的下一个最佳方法是使用 adb 查看 logcat。如果可以选择对设备进行物理访问,这是一种方便的方法,因为 Android 应用程序中的默认 UncaughtExceptionHandler 在终止进程之前会将整个堆栈跟踪打印到 Logcat,这意味着崩溃被有效地注销到开发人员可以访问的位置。

ANR 跟踪

ANR(应用程序未响应)发生在应用程序在相当长的一段时间内未响应用户输入时。这样做的明显效果是,从用户的角度来看,应用程序已经“冻结”了,这可能会非常令人沮丧。常见原因包括在主线程上执行磁盘读取/写入,以及其他长时间运行的任务,这会阻止用户界面更新以响应用户输入。

如果应用程序在前台,大约 5 秒后将显示一个对话框,允许用户终止应用程序。此时,包含 ANR 详细信息的跟踪将写入磁盘,从中可以检索到有价值的调试信息。同样,这需要对设备进行物理访问,除非您安装了支持 ANR 检测的崩溃报告 SDK。

墓碑

 当 Android 应用程序中发生 C/C++ 代码中的本机崩溃时,会写入Tombstone 崩溃日志。Android 平台将崩溃时所有正在运行的线程的跟踪写入 /data/tombstones,以及用于调试的其他信息,例如有关内存和打开文件的信息。就信息而言,墓碑最接近金属,因为它们将记录诸如原始内存地址之类的详细信息,因此除非您熟悉调试本机代码,否则理解起来可能会有些棘手。同样,墓碑需要物理访问根设备才能被读取。

如何从 Android 设备获取崩溃日志

作为所有这些步骤的先决条件,您应该已经 安装了 Android Studio 并将 命令行工具添加 到您的路径中。这些本地方法使用 adb 工具。您还应该连接一个 启用了开发人员选项的设备或模拟器。

注意:如果您习惯直接在 Android Studio 中使用 设备文件资源管理器 ,则可以直接从那里打开设备文件,而不是使用 adb pull。

异常堆栈跟踪

默认情况下,异常堆栈跟踪会打印到 Android 设备上的Logcat工具。可以通过以下步骤检索崩溃日志:

  1. 运行以下命令
adb logcat AndroidRuntime:E *:S
  1. 在设备上触发崩溃。堆栈跟踪将在终端中显示为新文本。
  2. 将终端输出保存到您选择的文件中以供稍后检查

如果设备最近发生了崩溃,您可以跳过第 2 步。这是因为 Logcat 保留了最近日志的缓冲区,其中应该包含异常。然而,这对时间很敏感——因此,如果您正在寻找一天前的崩溃,那么除非您使用诸如 Bugsnag 之类的崩溃报告工具,否则该信息可能会永远消失。

ANR 跟踪

  1. 在设备上触发 ANR。
  2. 运行以下命令,将目标替换为您选择的文件
adb pull /data/anr/traces.txt <destination>
  1. 通过打开保存的文件检查 ANR 崩溃日志中的信息

或者,您可以通过运行以下命令检查摘要 ANR 信息

adb logcat ActivityManager:E *:S

墓碑

  1. 根您的设备 或模拟器,以便您可以访问 tombstone 目录。(生根设备时要小心,因为此步骤可能会使您的手机变砖)
  2. 在设备上触发本机崩溃。
  3. 运行以下命令以确定设备上存在哪些 tombstone 崩溃日志
adb ls /data/tombstones
  1. 运行以下命令,将目标替换为您选择的文件。 tombstone_01 在此处显示为示例文件名,将在上一步中获得
adb pull /data/tombstones/tombstone_01 <destination>
  1. 通过打开保存的文件检查 Tombstone 崩溃日志中的信息

理解 Android 崩溃日志数据

异常堆栈跟踪

读取 JVM 堆栈跟踪起初可能会令人生畏,但通过将其分解为其组成部分,任务变得相当容易。让我们一步一步地完成它, 在示例应用程序中抛出以下RuntimeException :

2019-08-27 16:10:28.303 10773-10773/com.bugsnag.android.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.bugsnag.android.example, PID: 10773
    java.lang.RuntimeException: Fatal Crash
        at com.example.foo.CrashyClass.sendMessage(CrashyClass.java:10)
        at com.example.foo.CrashyClass.crash(CrashyClass.java:6)
        at com.bugsnag.android.example.ExampleActivity.crashUnhandled(ExampleActivity.kt:55)
        at com.bugsnag.android.example.ExampleActivity$onCreate$1.invoke(ExampleActivity.kt:33)
        at com.bugsnag.android.example.ExampleActivity$onCreate$1.invoke(ExampleActivity.kt:14)
        at com.bugsnag.android.example.ExampleActivity$sam$android_view_View_OnClickListener$0.onClick(ExampleActivity.kt)
        at android.view.View.performClick(View.java:5637)
        at android.view.View$PerformClick.run(View.java:22429)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

首先要开始的地方是我们崩溃日志的顶部。在这里,我们可以看到系统分配给正在执行的应用程序的进程 ID,以及包名称,这在关联通过 logcat 获得的其他信息时非常有用。在我们的示例应用程序中,包名称是“com.bugsnag.android.example”:

Process: com.bugsnag.android.example, PID: 10773

下一条有用的信息是异常类。在 Java/Kotlin 中,所有异常和错误都是扩展 Throwable 的类或 Throwable 的子类之一,每个异常类可以具有不同的语义含义。例如,如果程序进入意外状态,开发人员可能希望抛出 IllegalStateException,或者如果用户试图将 null 作为其名称保存,则可能希望抛出 IllegalArgumentException。在我们的例子中,我们抛出了一个 RuntimeException,其完全限定的类名如下所示:

java.lang.RuntimeException: Fatal Crash

错误消息也会打印到崩溃日志中,这对于提供额外的调试信息非常有用。在我们的例子中,我们刚刚提供了文本“Fatal Crash”,但如果我们需要更多信息以进行调试,我们同样可以在崩溃时传递变量的值。

崩溃日志中的下一件事是信息的重要部分 – 发生异常的线程的堆栈跟踪。查看顶部的堆栈帧通常可以让我们准确找到错误是从哪里引发的,下面的帧可以让我们观察崩溃时程序的状态。顶部堆栈帧如下:

com.example.foo.CrashyClass.sendMessage(CrashyClass.java:10)

我们可以立即看到其中包含一些非常有用的信息。我们得到了类“com.example.foo.CrashyClass”,因此我们可以打开源文件并在那里寻找错误。我们还获得了方法名称“sendMessage”和崩溃的行号 10,因此我们可以在源中准确定位异常是从哪里引发的。

从这一点开始理解崩溃日志是阅读下面的堆栈帧的情况,这些堆栈帧是按照最初调用方法的顺序排列的。考虑下面的例子:

at com.example.foo.CrashyClass.sendMessage(CrashyClass.java:10)
        at com.example.foo.CrashyClass.crash(CrashyClass.java:6)
        at com.bugsnag.android.example.ExampleActivity.crashUnhandled(ExampleActivity.kt:55)

从顶部开始,我们可以看出“sendMessage”是由“crash”调用的,而后者又被一个名为“crashUnhandled”的方法调用,这似乎是我们崩溃的根源。当然,完整的异常堆栈跟踪要复杂一些,还涉及到 Android 框架中的方法调用,但基本原理是一样的。

一个重要的注意事项是,在生产中,应用程序经常 被 Proguard 等工具混淆 ,这可能意味着原始符号不存在,并且堆栈跟踪变得难以理解。幸运的是,大多数崩溃报告服务都提供插件,这些插件会 自动上传一个映射文件 ,其中包含从生产应用程序中符号化崩溃报告所需的信息。

ANR 跟踪

ANR 跟踪包含大量信息。由于 ANR 可能是由多个应用程序争用有限数量的资源引起的,因此完整的崩溃日志包含多个不同进程的堆栈跟踪。当所有其他方法都失败时,此完整信息对于调试非常有用,但大多数情况下,打印到 Logcat 中的摘要 ANR 信息足以调试错误。考虑以下:

2019-08-27 16:12:57.301 1717-1733/system_process E/ActivityManager: ANR in com.bugsnag.android.example (com.bugsnag.android.example/.ExampleActivity)
    PID: 10859
    Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago.  Wait queue length: 33.  Wait queue head age: 6178.1ms.)
    Load: 0.32 / 0.62 / 0.37
    CPU usage from 323086ms to -1ms ago (2019-08-27 13:16:43.467 to 2019-08-27 16:12:54.131):
      5.7% 1717/system_server: 2.6% user + 3% kernel / faults: 21251 minor
      4.7% 10392/com.bugsnag.android.example: 1.3% user + 3.3% kernel / faults: 587 minor
      3.9% 2375/com.google.android.gms: 2.9% user + 0.9% kernel / faults: 71377 minor 51 major
      3.1% 16/ksoftirqd/2: 0% user + 3.1% kernel
      2.5% 2254/com.google.android.googlequicksearchbox:search: 1.1% user + 1.4% kernel / faults: 10193 minor
      1.2% 10427/kworker/u8:0: 0% user + 1.2% kernel
      0.9% 8990/kworker/u8:2: 0% user + 0.9% kernel
      0.8% 1342/surfaceflinger: 0.1% user + 0.7% kernel / faults: 35 minor
      0.5% 1344/adbd: 0% user + 0.4% kernel / faults: 8471 minor
      0.4% 1896/com.google.android.gms.persistent: 0.3% user + 0% kernel / faults: 1106 minor
      0.4% 1288/logd: 0.1% user + 0.3% kernel / faults: 43 minor
      0.3% 1806/com.android.systemui: 0.2% user + 0% kernel / faults: 404 minor
      0.2% 1916/com.android.phone: 0.1% user + 0% kernel / faults: 203 minor
      0.2% 1410/audioserver: 0% user + 0.1% kernel / faults: 119 minor
      0.1% 10429/kworker/u8:3: 0% user + 0.1% kernel
      0.1% 10378/com.google.android.apps.photos: 0.1% user + 0% kernel / faults: 426 minor
      0.1% 8/rcu_preempt: 0% user + 0.1% kernel
      0% 1396/jbd2/dm-0-8: 0% user + 0% kernel
      0% 2179/com.google.android.apps.nexuslauncher: 0% user + 0% kernel / faults: 802 minor 1 major
      0% 1409/zygote: 0% user + 0% kernel / faults: 857 minor
      0% 3951/com.android.defcontainer: 0% user + 0% kernel / faults: 265 minor
      0% 10137/kworker/u9:0: 0% user + 0% kernel
      0% 1987/wpa_supplicant: 0% user + 0% kernel
      0% 10205/com.google.android.apps.docs: 0% user + 0% kernel / faults: 50 minor
      0% 1378/dmcrypt_write: 0% user + 0% kernel
      0% 2111/com.google.process.gapps: 0% user + 0% kernel / faults: 356 minor
      0% 3882/com.android.printspooler: 0% user + 0% kernel / faults: 241 minor
      0% 8829/kworker/u9:2: 0% user + 0% kernel
      0% 9808/kworker/u9:4: 0% user + 0% kernel
      0% 19/migration/3: 0% user + 0% kernel
      0% 1420/rild: 0% user + 0% kernel
      0% 10138/kworker/u9:1: 0% user + 0% kernel
      0% 1339/lmkd: 0% user + 0% kernel
      0% 1419/netd: 0% user + 0% kernel / faults: 59 minor
      0% 1793/com.android.inputmethod.latin: 0% user + 0% kernel / faults: 12 minor
      0% 10146/com.android.gallery3d: 0% user + 0% kernel / faults: 95 minor
      0% 10181/android.process.acore: 0% user + 0% kernel / faults: 52 minor
      0% 1281/kworker/0:1H: 0% user + 0% kernel
      0% 10162/kworker/2:1: 0% user + 0% kernel
      0% 10348/com.google.android.partnersetup: 0% user + 0% kernel / faults: 92 minor
      0% 20/ksoftirqd/3: 0% user + 0% kernel
      0% 10308/android.process.media: 0% user + 0% kernel / faults: 16 minor
      0% 1336/healthd: 0% user + 0% kernel
      0% 1354/logcat: 0% user + 0% kernel
      0% 1709/hostapd: 0% user + 0% kernel
      0% 3/ksoftirqd/0: 0% user + 0% kernel
      0% 1341/servicemanager: 0% user + 0% kernel
      0% 2091/com.google.android.ext.services: 0% user + 0% kernel / faults: 10 minor
      0% 10475/com.google.android.apps.photos:CameraShortcut: 0% user + 0% kernel / faults: 29 minor
      0% 4/kworker/0:0: 0% user + 0% kernel
      0% 12/ksoftirqd/1: 0% user + 0% kernel
      0% 1422/fingerprintd: 0% user + 0% kernel
      0% 1591/dhcpclient: 0% user + 0% kernel
      0% 1706/ipv6proxy: 0% user + 0% kernel
      0% 1913/sdcard: 0% user + 0% kernel
      0% 2137/com.google.android.googlequicksearchbox:interactor: 0% user + 0% kernel / faults: 3 minor
      0% 687/kworker/1:1: 0% user + 0% kernel
      0% 1297/vold: 0% user + 0% kernel / faults: 10 minor
      0% 1413/installd: 0% user + 0% kernel / faults: 35 minor
      0% 1//init: 0% user + 0% kernel
      0% 11/migration/1: 0% user + 0% kernel
      0% 466

首先,崩溃日志包含有关系统上哪个进程遭受 ANR 的信息,并给出了进程 ID 和包名称,这在在更详细的 ANR 跟踪中找到适当的堆栈跟踪时很有用:

E/ActivityManager: ANR in com.bugsnag.android.example (com.bugsnag.android.example/.ExampleActivity)
    PID: 10859

Android 框架为我们提供了 ANR 的原因。在这种情况下,用户多次触摸屏幕,调度队列等待超过 6 秒,而对这些触摸事件没有显示可见的响应。这代表了一种非常糟糕的用户体验,因为从用户的角度来看,整个应用程序似乎会冻结:

Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 33. Wait queue head age: 6178.1ms.)

最后,我们得到了一些 CPU 负载信息。虽然一些 ANR 的原因很简单,例如在主线程上执行 IO,但情况并非总是如此。有时在低端设备上可能会发生 ANR,因为大量资源匮乏的应用程序争用 CPU,因此确定是否有其他应用程序与我们的应用程序同时使用大量资源会非常有帮助:

CPU usage from 323086ms to -1ms ago (2019-08-27 13:16:43.467 to 2019-08-27 16:12:54.131):
      5.7% 1717/system_server: 2.6% user + 3% kernel / faults: 21251 minor
      4.7% 10392/com.bugsnag.android.example: 1.3% user + 3.3% kernel / faults: 587 minor
      3.9% 2375/com.google.android.gms: 2.9% user + 0.9% kernel / faults: 71377 minor 51 major
      3.1% 16/ksoftirqd/2: 0% user + 3.1% kernel
      2.5% 2254/com.google.android.googlequicksearchbox:search: 1.1% user + 1.4% kernel / faults: 10193 minor

墓碑

与 ANR 痕迹一样,墓碑也包含大量无法完全遍历的信息。我们将考虑一个截断的示例,它在顶部附近显示最重要的信息:

ABI: 'x86'
pid: 15300, tid: 15300, name: android.example  >>> com.bugsnag.android.example <<<
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0xb19aa650
   eax b19aa650  ebx b19abfd8  ecx 00000005  edx b32a1230
   esi 99365d7e  edi bf9c2338
   xcs 00000073  xds 0000007b  xes 0000007b  xfs 0000003b  xss 0000007b
   eip b19aa688  ebp bf9c2148  esp bf9c2140  flags 00010296

backtrace:
   #00 pc 00000688  /data/app/com.bugsnag.android.example-2/lib/x86/libentrypoint.so (crash_write_read_only+40)
   #01 pc 000006ca  /data/app/com.bugsnag.android.example-2/lib/x86/libentrypoint.so (Java_com_bugsnag_android_example_ExampleActivity_doCrash+42)
   #02 pc 003e9d3c  /data/app/com.bugsnag.android.example-2/oat/x86/base.odex (offset 0x399000)

stack:
        bf9c2100  b32dc140  [anon:libc_malloc]
        bf9c2104  b3159f5c  /system/lib/libart.so
        bf9c2108  b315abb3  /system/lib/libart.so
        bf9c210c  b315ab82  /system/lib/libart.so
        bf9c2110  b315ab69  /system/lib/libart.so
        bf9c2114  b716ced4  /dev/ashmem/dalvik-LinearAlloc (deleted)
        bf9c2118  b328b400  [anon:libc_malloc]
        bf9c211c  b312b721  /system/lib/libart.so (_ZN3art14JniMethodStartEPNS_6ThreadE+17)
        bf9c2120  00430000
        bf9c2124  00590000
        bf9c2128  b328b400  [anon:libc_malloc]
        bf9c212c  b32a1230  [anon:libc_malloc]
        bf9c2130  b32b00c0  [anon:libc_malloc]
        bf9c2134  b328b400  [anon:libc_malloc]
        bf9c2138  00000043
        bf9c213c  b19aa66e  /data/app/com.bugsnag.android.example-2/lib/x86/libentrypoint.so (crash_write_read_only+14)
   #00  bf9c2140  bf9c2200
        bf9c2144  b19aa650  /data/app/com.bugsnag.android.example-2/lib/x86/libentrypoint.so
        bf9c2148  bf9c2178
        bf9c214c  b19aa6cb  /data/app/com.bugsnag.android.example-2/lib/x86/libentrypoint.so (Java_com_bugsnag_android_example_ExampleActivity_doCrash+43)
   #01  bf9c2150  00430000
        bf9c2154  00000013
        bf9c2158  05980a40
        bf9c215c  bf9c219c
        bf9c2160  b32a1230  [anon:libc_malloc]
        bf9c2164  0000000c
        bf9c2168  bf9c21cc
        bf9c216c  b2bc803f  /system/lib/libart.so (art_jni_dlsym_lookup_stub+15)
        bf9c2170  b328b400  [anon:libc_malloc]
        bf9c2174  0000000c
        bf9c2178  bf9c21cc
        bf9c217c  994aed3d  /data/app/com.bugsnag.android.example-2/oat/x86/base.odex
   #02  bf9c2180  b32a1230  [anon:libc_malloc]
        bf9c2184  bf9c219c
        bf9c2188  05980a40
        bf9c218c  00000001
        bf9c2190  b716ced4  /dev/ashmem/dalvik-LinearAlloc (deleted)
        bf9c2194  bf9c2c24
        bf9c2198  00000001
        bf9c219c  12c7b450  /dev/ashmem/dalvik-main space (deleted)
        bf9c21a0  00000006
        bf9c21a4  b31fbb74  /system/lib/libart.so
        bf9c21a8  bf9c2238
        bf9c21ac  b2f3a0a4  /system/lib/libart.so (_ZNK3art7OatFile8OatClass19GetOatMethodOffsetsEj+100)
        bf9c21b0  bf9c21cc
        bf9c21b4  99365d7e  /data/app/com.bugsnag.android.example-2/oat/x86/base.odex
        bf9c21b8  bf9c2338
        bf9c21bc  b2bc9263  /system/lib/libart.so (art_quick_invoke_stub+339)

分解它,我们在顶部给出了基本的崩溃日志信息,例如包名称和进程 ID。至关重要的是,因为这是一个本机错误并且可能受到 CPU 架构的影响,所以墓碑还包含设备的 ABI,在本例中是 x86,因为在模拟器上触发了崩溃:

ABI: 'x86'
pid: 15300, tid: 15300, name: android.example  >>> com.bugsnag.android.example <<<

我们获得了有关本机错误的信息,在这种情况下,这是由于引发了 SIGSEGV 信号。这包含触发故障的地址,以及汇编指令:

signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0xb19aa650
   eax b19aa650  ebx b19abfd8  ecx 00000005  edx b32a1230
   esi 99365d7e  edi bf9c2338
   xcs 00000073  xds 0000007b  xes 0000007b  xfs 0000003b  xss 0000007b
   eip b19aa688  ebp bf9c2148  esp bf9c2140  flags 00010296

跟踪的其余部分包含崩溃时正在执行的符号,可以使用应用程序的共享目标文件中的调试信息进行符号化,这是 Bugsnag 通过 gradle 插件集成自动实现的功能。

下一步

虽然在某些情况下手动获取 Android 设备的崩溃日志可能非常有用,但可以通过在应用程序中安装崩溃报告 SDK(例如 Bugsnag 的 Android SDK )来自动收集崩溃日志。崩溃报告 SDK 会自动检测 JVM 崩溃、NDK 崩溃和 ANR,并自动将有关错误的诊断报告发送到 Web 仪表板。

自动收集崩溃日志有几个优点。您可能无法始终访问设备,尤其是在最终用户的设备上发生崩溃时,或者如果您使用的是第 3 方质量保证公司,他们可能没有安装必要的工具来手动获取崩溃日志。测试装置。崩溃报告 SDK 可自动报告您的生产应用程序中的错误,因此您可以立即了解有多少用户受到错误影响,并确定如何使用诊断信息修复它。

崩溃报告 SDK 的一大优势是可以将自定义元数据附加到错误报告中。设备上的崩溃日志受到 Android 框架可以收集的信息量的限制,但对于 3rd 方工具则没有这样的限制。例如,Bugsnag 将自动捕获 Android 上 生命周期事件 和常见 系统广播的面包屑,这可以帮助追踪与生命周期事件中的意外状态相关的棘手错误。

崩溃报告服务还提供强大的 搜索和细分。也许错误只发生在某些操作系统版本上,或者应用内购买的严重错误正在影响付费客户的转化率。通过将自定义元数据添加到错误报告中,可以对重要错误进行分段和搜索,以便您可以优先考虑为您的业务带来价值的内容,而不是浪费时间自己手动整理来自数十台设备的崩溃日志。

can-we-make-sense-of-android-crash-logs-lets-find-out