崩溃

每当由于未处理的异常或信号导致意外退出时,Android 应用就会崩溃。使用 Java 或 Kotlin 编写的应用在抛出未处理的异常时会崩溃,该异常由 Throwable 类表示。使用机器码或 C++ 编写的应用在执行期间遇到未处理的信号(例如 SIGSEGV)时会崩溃。

当应用崩溃时,Android 会终止应用的进程并显示一个对话框,告知用户应用已停止,如图 1 所示。

An app crash on an Android device

图 1. Android 设备上的应用崩溃

应用不必在前景运行才会崩溃。任何应用组件,即使是后台运行的组件(例如广播接收器或内容提供程序),都可能导致应用崩溃。这些崩溃对于用户来说通常令人困惑,因为他们当时并没有积极使用您的应用。

如果您的应用遇到崩溃,您可以使用本页中的指南诊断和解决问题。

检测问题

您可能并不总是知道用户在使用您的应用时是否遇到崩溃。如果您已经发布了您的应用,您可以使用 Android 指标查看应用的崩溃率。

Android 指标

Android 指标可以帮助您监控和提高应用的崩溃率。Android 指标衡量几个崩溃率

  • 崩溃率:每天活跃用户中遇到任何类型崩溃的百分比。
  • 用户感知崩溃率:每天活跃用户中在积极使用应用时遇到至少一次崩溃的百分比(用户感知崩溃)。如果应用正在显示任何活动或正在执行任何 前台服务,则该应用被视为处于积极使用状态。

  • 多次崩溃率:每天活跃用户中遇到至少两次崩溃的百分比。

每天活跃用户是指在一天中使用您应用的唯一用户,在单个设备上,可能会跨多个会话。如果用户在一天中使用您的应用超过一台设备,则每台设备都会计入该天的活跃用户数。如果多个用户在一天中使用同一台设备,则计为一名活跃用户。

用户感知崩溃率是一个核心指标,这意味着它会影响您的应用在 Google Play 上的发现率。它很重要,因为该指标计入的崩溃始终发生在用户与应用交互时,导致最大的中断。

Play 为此指标定义了两个不良行为阈值

  • 总体不良行为阈值:在所有设备型号上,至少 1.09% 的每天活跃用户遇到用户感知崩溃。
  • 每个设备的不良行为阈值:至少 8% 的每天活跃用户遇到用户感知崩溃,针对单个设备型号

如果您的应用超过了总体不良行为阈值,则它很可能在所有设备上都难以被发现。如果您的应用在某些设备上超过了每个设备的不良行为阈值,则它很可能在这些设备上难以被发现,并且您的商店列表中可能会显示警告。

当您的应用出现过多的崩溃时,Android 指标可以通过 Play 管理中心 向您发出警报。

有关 Google Play 如何收集 Android 指标数据的更多信息,请参阅 Play 管理中心 文档。

诊断崩溃

一旦确定您的应用报告了崩溃,下一步就是诊断它们。解决崩溃可能很困难。但是,如果您能确定崩溃的根本原因,您很可能就能找到解决方案。

有很多情况会导致您的应用崩溃。一些原因很明显,例如检查空值或空字符串,但另一些原因则比较微妙,例如向 API 传递无效参数,甚至复杂的线程交互。

Android 上的崩溃会生成堆栈跟踪,它是您的程序在崩溃那一刻之前调用的嵌套函数序列的快照。您可以在 Android 指标 中查看崩溃堆栈跟踪。

如何阅读堆栈跟踪

修复崩溃的第一步是确定崩溃发生的位置。如果您使用的是 Play 管理中心,您可以使用报告详细信息中提供的堆栈跟踪,或者使用 logcat 工具的输出。如果您没有堆栈跟踪可用,您应该在本地重现崩溃,方法是手动测试应用或联系受影响的用户,并在使用 logcat 的同时重现崩溃。

以下跟踪显示了一个使用 Java 编程语言编写的应用的崩溃示例

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
at android.view.View.performClick(View.java:6134)
at android.view.View$PerformClick.run(View.java:23965)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6440)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
--------- beginning of system

堆栈跟踪显示了调试崩溃至关重要的两条信息

  • 抛出的异常类型。
  • 抛出异常的代码部分。

抛出的异常类型通常是对发生错误的强烈提示。查看它是否为 IOExceptionOutOfMemoryError 或其他类型,然后查找有关异常类的文档。

抛出异常的源文件中的类、方法、文件和行号显示在堆栈跟踪的第二行。对于每个调用的函数,另一行会显示之前的调用位置(称为堆栈帧)。通过向上遍历堆栈并检查代码,您可能会找到一个传递了错误值的位置。如果您的代码未出现在堆栈跟踪中,则很可能您在某个地方向异步操作传递了无效参数。您通常可以通过检查堆栈跟踪的每一行、找到您使用的任何 API 类、确认您传递的参数是否正确以及确认您是否从允许的位置调用它来弄清楚发生了什么。

具有 C 和 C++ 代码的应用的堆栈跟踪的工作原理基本相同。

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp  >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000007da81396c0  x1  0000007fc91522d4  x2  0000000000000001  x3  000000000000206e
    x4  0000007da8087000  x5  0000007fc9152310  x6  0000007d209c6c68  x7  0000007da8087000
    x8  0000000000000000  x9  0000007cba01b660  x10 0000000000430000  x11 0000007d80000000
    x12 0000000000000060  x13 0000000023fafc10  x14 0000000000000006  x15 ffffffffffffffff
    x16 0000007cba01b618  x17 0000007da44c88c0  x18 0000007da943c000  x19 0000007da8087000
    x20 0000000000000000  x21 0000007da8087000  x22 0000007fc9152540  x23 0000007d17982d6b
    x24 0000000000000004  x25 0000007da823c020  x26 0000007da80870b0  x27 0000000000000001
    x28 0000007fc91522d0  x29 0000007fc91522a0
    sp  0000007fc9152290  lr  0000007d22d4e354  pc  0000007cba01b640

backtrace:
  #00  pc 0000000000042f89  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
  #01  pc 0000000000000640  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
  #02  pc 0000000000065a3b  /system/lib/libc.so (__pthread_start(void*))
  #03  pc 000000000001e4fd  /system/lib/libc.so (__start_thread)

如果您在原生堆栈跟踪中没有看到类和函数级信息,您可能需要 生成原生调试符号文件 并将其上传到 Google Play 管理中心。有关更多信息,请参阅 反混淆崩溃堆栈跟踪。有关原生崩溃的一般信息,请参阅 诊断原生崩溃

重现崩溃的提示

您可能无法仅仅通过启动模拟器或将设备连接到计算机来完全重现问题。开发环境往往拥有更多资源,例如带宽、内存和存储空间。使用异常类型确定可能稀缺的资源,或找到 Android 版本、设备类型或您的应用版本之间的相关性。

内存错误

如果您遇到了 OutOfMemoryError,那么您可以创建一个内存容量低的模拟器来进行测试。图 2 显示了 AVD 管理器设置,您可以在其中控制设备的内存量。

Memory setting on AVD manager

图 2. AVD 管理器上的内存设置

网络异常

由于用户经常进出移动或 WiFi 网络覆盖范围,因此在应用程序中,网络异常通常不应被视为错误,而应被视为意外发生的正常运行条件。

如果您需要重现网络异常,例如 UnknownHostException,那么请尝试在应用程序尝试使用网络时打开飞行模式。

另一个选择是通过选择网络速度模拟和/或网络延迟来降低模拟器中的网络质量。您可以使用 AVD 管理器上的速度延迟设置,或者您可以使用 -netdelay-netspeed 标志启动模拟器,如下面的命令行示例所示

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

此示例在所有网络请求上设置 20 秒的延迟,以及 14.4 Kbps 的上传和下载速度。有关模拟器命令行选项的更多信息,请参阅 从命令行启动模拟器

使用 logcat 读取

一旦您能够重现崩溃的步骤,您可以使用像 logcat 这样的工具来获取更多信息。

logcat 输出将显示您打印的其他日志消息,以及系统中的其他日志消息。不要忘记关闭您添加的任何额外的 Log 语句,因为在应用程序运行时打印它们会浪费 CPU 和电池电量。

防止由空指针异常引起的崩溃

空指针异常(由运行时错误类型 NullPointerException 标识)发生在您尝试访问空对象时,通常是通过调用其方法或访问其成员。空指针异常是 Google Play 上应用程序崩溃的主要原因。 空的目的是表示对象丢失 - 例如,它尚未创建或分配。为了避免空指针异常,您需要确保在调用其方法或尝试访问其成员之前,您正在使用的对象引用不为空。如果对象引用为空,请妥善处理这种情况(例如,在对对象引用执行任何操作之前退出方法并向调试日志写入信息)。

由于您不希望对调用的每个方法的每个参数进行空检查,因此您可以依赖 IDE 或对象类型来表示可空性。

Java 编程语言

以下部分适用于 Java 编程语言。

编译时警告

使用 @Nullable@NonNull 注释您的方法参数和返回值,以从 IDE 接收编译时警告。这些警告提示您预期一个可空对象

Null pointer exception warning

这些空检查用于您知道可能为空的对象。在 @NonNull 对象上发生的异常表明您的代码中存在需要解决的错误。

编译时错误

由于可空性应该是有意义的,因此您可以将其嵌入您使用的类型中,以便对空进行编译时检查。如果您知道一个对象可以为空并且可空性应该被处理,您可以将其包装在一个像 Optional 这样的对象中。您应该始终优先选择表示可空性的类型。

Kotlin

在 Kotlin 中,可空性 是类型系统的一部分。例如,变量需要从一开始就被声明为可空或不可空。可空类型用 ? 标记

// non-null
var s: String = "Hello"

// null
var s: String? = "Hello"

不可空变量不能被分配空值,可空变量需要在被用作不可空值之前检查可空性。

如果您不想显式检查空,可以使用 ?. 安全调用运算符

val length: Int? = string?.length  // length is a nullable int
                                   // if string is null, then length is null

作为最佳实践,请确保您处理可空对象的空情况,否则您的应用程序可能会进入意外状态。如果您的应用程序不再因 NullPointerException 而崩溃,您将不知道这些错误的存在。

以下是检查空的一些方法

  • if 检查

    val length = if(string != null) string.length else 0
    

    由于智能转换和空检查,Kotlin 编译器知道字符串值不为空,因此它允许您直接使用引用,而无需使用安全调用运算符。

  • ?: Elvis 运算符

    此运算符允许您声明“如果对象不为空,则返回对象;否则,返回其他内容”。

    val length = string?.length ?: 0
    

您仍然可以在 Kotlin 中遇到 NullPointerException。以下是最常见的情况

  • 当您显式抛出 NullPointerException 时。
  • 当您使用 空断言 !! 运算符 时。此运算符将任何值转换为不可空类型,如果该值为 null,则抛出 NullPointerException
  • 当访问平台类型的空引用时。

平台类型

平台类型是来自 Java 的对象声明。 这些类型是特殊处理的;空检查没有那么严格,因此不可空保证与 Java 中相同。当您访问平台类型引用时,Kotlin 不会创建编译时错误,但这些引用可能导致运行时错误。请参阅以下来自 Kotlin 文档的示例

val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
                                                       // exception if item == null

当平台值被分配给 Kotlin 变量时,Kotlin 依赖于类型推断,或者您可以定义要期望的类型。确保来自 Java 的引用的正确可空性状态的最佳方法是在 Java 代码中使用可空性注释(例如,@Nullable)。Kotlin 编译器将这些引用表示为实际的可空或不可空类型,而不是平台类型。

Java Jetpack API 已根据需要使用 @Nullable@NonNull 进行了注释,并在 Android 11 SDK 中采取了类似的方法。来自此 SDK 的类型,在 Kotlin 中使用,将被表示为正确可空或不可空类型。

由于 Kotlin 的类型系统,我们看到应用程序在 NullPointerException 崩溃方面有了显著减少。例如,Google Home 应用程序在将新的功能开发迁移到 Kotlin 的一年中,由空指针异常引起的崩溃减少了 30%。