每当由于未处理的异常或信号导致意外退出时,Android 应用程序就会崩溃。使用 Java 或 Kotlin 编写的应用程序如果抛出未处理的异常,则会崩溃,该异常由 Throwable
类表示。使用机器代码或 C++ 编写的应用程序如果在其执行期间出现未处理的信号,例如 SIGSEGV
,则会崩溃。
当应用程序崩溃时,Android 会终止应用程序的进程并显示一个对话框,让用户知道该应用程序已停止,如图 1 所示。
应用程序不需要在前景运行才能崩溃。任何应用程序组件,即使是在后台运行的广播接收器或内容提供者等组件,也可能导致应用程序崩溃。这些崩溃对于用户来说经常令人困惑,因为他们没有积极地使用您的应用程序。
如果您的应用程序遇到崩溃问题,您可以使用本页中的指南来诊断和解决问题。
检测问题
您可能并不总是知道用户在使用您的应用程序时遇到崩溃问题。如果您已发布应用程序,可以使用 Android 指标查看应用程序的崩溃率。
Android 指标
Android 指标可以帮助您监控和提高应用程序的崩溃率。Android 指标测量几种崩溃率
- 崩溃率: 每天使用您的应用程序的活跃用户中遇到任何类型崩溃的百分比。
用户感知的崩溃率: 每天使用您的应用程序的活跃用户中在积极使用您的应用程序时遇到至少一次崩溃的百分比(用户感知的崩溃)。如果应用程序正在显示任何活动或执行任何 前景服务,则该应用程序被视为处于积极使用状态。
多次崩溃率: 每天使用您的应用程序的活跃用户中遇到至少两次崩溃的百分比。
每日活跃用户是指在单日内使用您应用程序的唯一用户,该用户可能在单台设备上进行多次会话。如果用户在单日内使用您的应用程序超过一台设备,则每台设备都会为该日的活跃用户数量做出贡献。如果多个用户在单日内使用同一台设备,则计为一个活跃用户。
用户感知崩溃率是一个核心指标,因为它会影响您的应用程序在 Google Play 上的发现率。它很重要,因为计算的崩溃始终发生在用户与应用程序交互时,造成最大的干扰。
Play 针对此指标定义了两个不良行为阈值
- 总体不良行为阈值: 至少 1.09% 的每日活跃用户在所有设备型号上都遇到用户感知的崩溃。
- 每设备不良行为阈值: 至少 8% 的每日活跃用户在单个设备型号上遇到用户感知的崩溃。
如果您的应用程序超过总体不良行为阈值,则它在所有设备上的发现率可能会降低。如果您的应用程序在某些设备上超过每设备不良行为阈值,则它在这些设备上的发现率可能会降低,并且您的商店列表可能会显示警告。
当您的应用程序出现过多的崩溃时,Android 指标可以通过 Play Console 向您发出警报。
有关 Google Play 如何收集 Android 指标数据的更多信息,请参阅 Play Console 文档。
诊断崩溃
一旦您确定您的应用程序报告了崩溃,下一步就是诊断它们。解决崩溃可能很困难。但是,如果您能够确定崩溃的根本原因,则很可能能够找到解决方法。
许多情况会导致应用程序崩溃。有些原因很明显,比如检查空值或空字符串,但另一些原因则比较微妙,比如向 API 传递无效参数,甚至复杂的线程交互。
Android 上的崩溃会产生堆栈跟踪,它是在程序崩溃前的时刻,程序中调用的嵌套函数序列的快照。您可以在 Android 指标 中查看崩溃堆栈跟踪。
如何阅读堆栈跟踪
修复崩溃的第一步是确定发生崩溃的位置。如果您使用的是 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
堆栈跟踪显示了调试崩溃至关重要的两部分信息
- 抛出的异常类型。
- 抛出异常的代码部分。
抛出的异常类型通常是对错误原因的非常强烈的提示。查看它是否是 IOException
、OutOfMemoryError
还是其他,并找到有关异常类的文档。
抛出异常的源文件的类、方法、文件和行号显示在堆栈跟踪的第二行。对于每个调用的函数,另一行显示前一个调用站点(称为堆栈帧)。通过向上遍历堆栈并检查代码,您可能会找到一个传递了错误值的位置。如果您的代码没有出现在堆栈跟踪中,则可能是您在某个地方将无效参数传递给了异步操作。您通常可以通过检查堆栈跟踪的每一行,找到您使用的任何 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 管理器设置,您可以在其中控制设备的内存量。
网络异常
由于用户经常进出移动或 WiFi 网络覆盖范围,因此在应用程序中,网络异常通常不应该被视为错误,而应被视为意外发生的正常操作条件。
如果您需要重现网络异常,例如 UnknownHostException
,那么尝试在您的应用程序尝试使用网络时打开飞行模式。
另一种选择是通过选择网络速度模拟和/或网络延迟来降低模拟器中的网络质量。您可以使用 AVD 管理器上的速度和延迟设置,或者您可以使用 -netdelay
和 -netspeed
标志启动模拟器,如以下命令行示例所示
emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm
此示例为所有网络请求设置 20 秒的延迟,以及 14.4 Kbps 的上传和下载速度。有关模拟器命令行选项的更多信息,请参阅 从命令行启动模拟器。
使用 logcat 读取
一旦您能够掌握重现崩溃的步骤,您可以使用 logcat
等工具来获取更多信息。
logcat 输出将向您显示您打印的其他日志消息,以及系统中的其他消息。不要忘记关闭您添加的任何额外的 Log
语句,因为打印它们会在应用程序运行时浪费 CPU 和电池。
防止由空指针异常引起的崩溃
空指针异常(由运行时错误类型 NullPointerException
识别)发生在您尝试访问一个为 null 的对象时,通常是通过调用它的方法或访问它的成员来实现的。空指针异常是 Google Play 上应用程序崩溃的最大原因。 null 的目的是表示该对象不存在——例如,它尚未创建或分配。为了避免空指针异常,您需要确保在调用其方法或尝试访问其成员之前,您正在使用的对象引用非空。如果对象引用为空,则很好地处理这种情况(例如,在对对象引用执行任何操作之前退出方法,并将信息写入调试日志)。
因为您不希望为每个调用的方法的每个参数进行空检查,所以您可以依靠 IDE 或对象类型来表示可空性。
Java 编程语言
以下部分适用于 Java 编程语言。
编译时警告
使用 @Nullable
和 @NonNull
对方法的参数和返回值进行注释,以便从 IDE 接收编译时警告。这些警告会提示您预期一个可空对象
这些空检查适用于您知道可能为 null 的对象。 @NonNull
对象上的异常表示代码中需要解决的错误。
编译时错误
由于可空性应该是有意义的,因此您可以将其嵌入到使用的类型中,以便对 null 进行编译时检查。如果您知道一个对象可以为 null 并且应该处理可空性,您可以将其包装在一个类似于 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
作为最佳实践,请确保您为可空对象处理 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%。