崩溃

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 会衡量多种崩溃率

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

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

每日活跃用户是指在一天内,在单个设备上使用您的应用的唯一用户,可能会有多个会话。如果用户在一天内使用您的应用不止一台设备,那么每台设备都会计入当天的活跃用户数。如果多个用户在同一天使用同一设备,则计为一名活跃用户。

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

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

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

如果您的应用超出总体不良行为阈值,则可能在所有设备上都降低可发现性。如果您的应用在某些设备上超出单设备不良行为阈值,则可能在这些设备上降低可发现性,并且您的商品详情可能会显示警告。

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

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

诊断崩溃

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

有许多情况可能导致您的应用崩溃。有些原因很明显,例如检查空值或空字符串,但其他原因则更为微妙,例如将无效参数传递给 API,甚至是复杂的多线程交互。

Android 上的崩溃会生成堆栈轨迹,这是程序中嵌套函数调用序列直到崩溃那一刻的快照。您可以在 Android vitals 中查看崩溃堆栈轨迹。

如何读取堆栈轨迹

修复崩溃的第一步是确定其发生的位置。如果您正在使用 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%。