崩溃

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

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

An app crash on an Android device

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

应用无需在前台运行即可崩溃。任何应用组件(即使是像广播接收器或内容提供商这样的在后台运行的组件)都可能导致应用崩溃。这些崩溃对于用户来说通常令人困惑,因为他们当时并未积极使用您的应用。

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

检测问题

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

Android vitals

Android vitals 可帮助您监控和改善应用的崩溃率。Android vitals 会衡量多个崩溃率

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

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

每日活跃用户是指在一天内在一个设备上使用您的应用的唯一用户,可能包含多个会话。如果用户在一天内在一个以上设备上使用您的应用,则每个设备都会计入当天的活跃用户数。如果多个用户在一天内使用同一设备,则计为一个活跃用户。

用户感知的崩溃率是一个核心指标,这意味着它会影响您的应用在 Google Play 上的可发现性。它很重要,因为其计数的崩溃始终发生在用户使用应用时,造成最大的干扰。

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

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

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

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

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

诊断崩溃

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

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

Android 应用崩溃会产生堆栈跟踪,它是程序崩溃前调用的嵌套函数序列的快照。您可以在 Android Vitals 中查看崩溃堆栈跟踪。

如何阅读堆栈跟踪

修复崩溃的第一步是确定崩溃发生的位置。如果您使用的是 Play Console,则可以使用报告详细信息中提供的堆栈跟踪,或者使用 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 Console。有关更多信息,请参阅 反混淆崩溃堆栈跟踪。有关原生崩溃的常规信息,请参阅 诊断原生崩溃

重现崩溃的技巧

仅仅启动模拟器或将设备连接到计算机可能无法重现问题。开发环境往往拥有更多资源,例如带宽、内存和存储空间。使用异常类型来确定哪些资源可能不足,或者找到 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 首页应用程序在将新功能开发迁移到 Kotlin 的那一年,因空指针异常导致的崩溃减少了 30%。