干货|安卓应用崩溃捕获方案——xCrash

  

导读

2019 年,爱奇艺在 GitHub 上开源了 xCrash。这是一个比较完整的安卓 APP 崩溃捕获 SDK,它能在 App 进程崩溃时,在你指定的目录中生成 tombstone 文件(格式与系统的 tombstone 文件类似)。它支持捕获 native 崩溃和 Java 崩溃;支持安卓 4.0 - 9.0;支持 armeabi,armeabi-v7a,arm64-v8a,x86 和 x86_64。

依托于爱奇艺安卓 APP 上亿的日活用户数据,xCrash 在兼容性、稳定性、功能完整性等方面不断地自我完善。目前 xCrash 已被应用于爱奇艺、爱奇艺极速版、爱奇艺动画屋、奇秀、爱奇艺 VR 影院、叭哒漫画等 20 余款爱奇艺的安卓 APP 中。

问题概述

在移动端 APP 的各种质量问题中,最严重的可能就是 APP 崩溃闪退了。

从安卓 APP 开发的角度,Java 崩溃捕获相对比较容易,JVM 给 Java 字节码提供了一个受控的运行环境,同时也提供了完善的 Java 崩溃捕获机制。Native 崩溃的捕获和处理相对比较困难,安卓系统的debuggerd 守护进程会为 native 崩溃自动生成详细的崩溃描述文件(tombstone)。

在开发调试阶段,可以通过系统提供的 bugreport 工具获取 tombstone 文件(或者将设备 root 后也可以拿到)。但是对于发布到线上的安卓 APP,如何获取 tombstone 文件,安卓操作系统本身并没有提供这样的功能。这个问题一直是安卓 native 崩溃分析和移动端 APM 系统的痛点之一。

Native 崩溃介绍

信号

Native 崩溃发生在机器指令运行的层面。比如:APP 中的 so 库、系统的 so 库、JVM 本身等等。如果这部分程序做了 Linux kernel 认为不可接受的事情(比如:除数为零、让 CPU 执行它无法识别的指令等),kernel 就会向 APP 中对应的线程发送相应的信号(signal),这些信号的默认处理方式是杀死整个进程。用户态进程也可以发送 signal 终止其他进程或自身。这些致命的信号分为 2 类,主要有:

SIGFPE: 除数为零。

SIGILL: 无法识别的 CPU 指令。

SIGSYS: 无法识别的系统调用(system call)。

SIGSEGV: 错误的虚拟内存地址访问。

SIGBUS: 错误的物理设备地址访问。

SIGABRT: 调用 abort() / kill() / tkill() / tgkill() 自杀,或被其他进程通过 kill() / tkill() / tgkill() 他杀。

信号处理函数

干货|安卓APP崩溃捕获方案——xCrash

Naive 崩溃捕获需要注册这些信号的处理函数(signal handler),然后在信号处理函数中收集数据。

因为信号是以“中断”的方式出现的,可能中断任何 CPU 指令序列的执行,所以在信号处理函数中,只能调用“异步信号安全(async-signal-safe)”的函数。例如malloc()、calloc()、free()、snprintf()、gettimeofday() 等等都是不能使用的,C++ STL/boost 也是不能使用的。

所以,在信号处理函数中我们只能不分配堆内存,需要使用堆内存只能在初始化时预分配。如果要使用不在异步信号安全白名单中的 libc/bionic 函数,只能直接调用 system call 或者自己实现。

进程崩溃前的极端情况

当崩溃捕获逻辑开始运行时,会面对很多糟糕的情况,比如:栈溢出、堆内存不可用、虚拟内存地址耗尽、FD 耗尽、Flash 空间耗尽等。有时,这些极端情况的出现,本身就是导致进程崩溃的间接原因。

我们需要预先用 sigaltstack() 为 signal handler 分配专门的栈内存空间,否则当遇到栈溢出时,signal handler 将无法正常运行。

内存泄露很容易导致虚拟内存地址耗尽,特别是在 32 位环境中。这意味着在 signal handler 中也不能使用类似 mmap() 的调用。

FD 泄露是常见的导致进程崩溃的间接原因。这意味着在 signal handler 中无法正常的使用依赖于 FD 的操作,比如无法 open() + read() 读?proc 中的各种信息。为了不干扰 APP 的正常运行,我们仅仅预留了一个 FD,用于在崩溃时可靠的创建出“崩溃信息记录文件”。

干货|安卓应用崩溃捕获方案——xCrash