崩溃问题是衡量 App 质量的决定性考核标准。Android 系统会输出各种相应的 log 日志,很大程度上降低了工程师 debug 崩溃问题的难度。如果要给 crash 日志进行分类,可以分为2大类:JVM 异常(Exception)堆栈信息和 native 代码崩溃日志。
JVM 异常堆栈信息
Java 中异常(Exception)分两种:检查异常(checked Exception)和非检查异常(unchecked Exception)
检查异常是在代码编译时期,Android Studio 就会提示代码有错误,无法通过编译。比如 IOException。如果没有在代码中将这些异常 catch,而是直接抛出,最终也有可能导致程序崩溃。
非检查异常包括 error 和运行时异常(RuntimeException)。AS 并不会在编译时期提示这些异常信息,而是在程序运行时期因为代码错误而直接导致程序崩溃。比如 OOM 或者空指针异常(NPE)。
Java 异常
对于上述两种异常,我们都可以使用 UncaughtExceptionHandler 来进行捕获操作。它是 Thread 的一个内部接口,定义如下
对于传入的 Thread,如果因为未捕获异常而导致被终止,uncaughtException 则会被调用。我们可以通过它来间接捕获程序异常,并进行异常信息的记录工作,或者给出更友好的异常提示信息。
自定义异常处理类
自定义类实现 UncaughtExceptionHandler 接口,并实现 uncaughtException() 方法。如下代码所示
需要注意的几点,自定义异常处理类中,需要持有线程默认异常处理类,如图中红框处所示。这样做的目的是在自定义异常处理类无法处理或者处理异常失败时,还可以将异常交给系统做默认处理。如果自定义异常处理类成功处理异常,需要进行页面跳转或者将程序进程杀死,否则程序会一直卡死在崩溃界面,并弹出无响应对话框。在上面代码中通过 CrashDisplayActivity 页面来展示所捕获的异常详细信息。
一般情况下,在 handleException() 方法中需要做以下几件事情:
1)收集 crash 的现场相关信息。如下面方法获取当前 App 的版本信息,以及所有手机设备的相关信息
2)日志的记录工作,将收集到的信息保存在本地。比如以文件的方式保存。从下图中可以看出,除了我们自己收集的日志,还需要将系统抛出的异常信息也保持到文件中,方便后续开发人员分析问题原因。
使用自定义异常处理类
上面我们提到的 LagouExceptionHandler 自定义异常处理类定义好之后,就可以将其初始化。并将主线程注册到 LagouExceptionHandler 中。如下
最终,报错的 crash 日志信息格式如下图所示
需要注意的是,因为使用了文件写操作,所以需要动态申请文件操作的权限。
native 异常
当程序中的 native 代码发生崩溃时,系统会在 /data/tombstones/ 目录下保存一份详细的崩溃日志信息。如果一个 native crash 是必现的,不妨在模拟器上重现 bug。并将 /data/tombstones 中的崩溃日志拉到本地电脑中加以分析。
比如,创建一个模拟 native crash 的项目 LagouNativeCrash。项目结构如下
MainActivity 中有一个点击按钮 Button,当点击此按钮时,会调用 crash() 方法触发 native 代码崩溃。
native crash 在 native-lib.cpp 文件中声明,如下
当点击 Button 触发 native 崩溃之后,系统会在 /data/tombstones 目录下生成 tombstones 日志文件。可以在此日志文件中查看详细的保存信息。如下所示
但是,如果 native crash 是偶发现象,并且在模拟器上一时难以复现。那么就需要将工作交给测试工程师,在真机上尝试复现。
这种情况下就需要一种机制,将 native crash 现场的日志信息保存到可以访问的手机目录中。目前比较成熟,使用也比较广泛的是谷歌的 Breakpad。Breadpad 是一个跨平台的开源库,也可以在其 Breakpad Github 上下载自己编译,并通过 JNI 的方式引入到项目中。
NDK 导入 Breakpad
在 Breakpad GitHub 官网上,有一个 README Android 的介绍文件,这个文件专门介绍了如何在 Android 项目中导入 Breakpad。我们可以直接使用 cmake 方式将其编译为一个静态库。
在捕获 native crash 之前,需要初始化 Breakpad。主要是设置 Breakpad 保存 crash 日志的路径,如下所示
图中传入的 path 就是 Breakpad 保存日志的文件目录,一般情况下保存在外置 SDK 目录。
初始化好之后,就可以在我们自己的 native 业务层模拟一个崩溃现场。Breakpad 会自动捕获这次 crash,并将生成的 crash 信息保存在所设置的目录中。
breadpad 生成的文件是 .dmp 文件,需要将其进行转换,如下所示
线上崩溃日志捕获
上面介绍了 java 和 native 崩溃的捕获都是基于现场能够复现 bug 的前提。但是,对于线上用户,这种操作方式是不太现实的。首先,不可能将一个 debug 版本的 APK 安装到终端用户的手机上;其次,不太可能要求用户操作一遍手机并将某一目录下的文件发送给开发人员。
对于大多数公式来说,针对线上版本没有必要自己实现一个抓取 log 的平台系统。最快速的实现方式就是集成第三方 SDK。目前比较成熟,采用也比较多的就是腾讯的 Bugly。Bugly 能够满足线上版本捕获 crash 的所有需求,包括 java 层和 native 层的 crash 都可以获取相应的日志。并且每天 Bugly 都会邮件通知上一天的崩溃日志,方便测试和开发统计 bug 的分布以及崩溃率。
其它的 crash 上报工具,比如 XCrash 和 Sentry。
这两者比 Bugly 好的地方就是除了自动拦截界面崩溃事件,还可以主动上报错误信息。以 XCrash 为例,基本使用如下所示
可以看出 XCrash 的使用更加灵活,工程师的掌控性更高。可以通过设置不同的过滤方式针对性的上报相应的 crash 日志。并且在捕获到 crash 之后可以加入自定义的操作,比如本地保存日志或者直接进行网络上传。
Sentry 还有一个好处就是可以通过设置过滤,来判断是否上报 crash 日志。这对于 SDK 的开发人员是很有用的,比如一些 SDK 的开发商只是想收集自身 SDK 引入的 crash,对于用户的其他操作导致的 crash 进行过滤。这种情况就可以考虑集成 Sentry。
总结
本次主要介绍了 Android 崩溃的相关知识。对于 Android 工程师来说,crash 可以分为2类:Java 层和 Native 层
Java 层一般通过自定义 UncaughtExceptionHandler 进行异常拦截
Native 层可以考虑集成谷歌的 breakpad 进行捕获,并保存日志在本地
最后介绍了几个线上捕获 crash 的工具:Bugly、XCrash 和 Sentry。