4. Android系统Crash处理流程归纳总结

做Crash捕获,如果不懂系统底层是怎么处理Crash的,那你写出来的捕获方案大概率有坑。我见过不少同学直接在Native层写个signal handler就觉得完事了,结果线上跑着跑着发现tombstone文件没生成,或者捕获到的堆栈残缺不全。

说白了,Android系统处理Crash有一套固化的内核级链路。从Linux信号投递,到debuggerd介入,再到tombstoned落盘,每个环节都有它存在的理由。这一章我就把这套流程掰开揉碎了讲清楚。

4.1 两类Crash的根本差异

Java Crash和Native Crash虽然都叫"崩溃",但底层的处理路径完全不同。我刚开始做这块时也以为它们只是语言层面的区别,后来追过源码才明白——这是两套机制。

对比维度 Java Crash Native Crash
触发机制 未捕获异常(Throwable) 信号(Signal)
处理组件 Thread.UncaughtExceptionHandler Signal Handler / debuggerd
关键进程 app_process / Zygote debuggerd / tombstoned
输出产物 logcat 日志 tombstone 文件 + logcat
是否阻塞 处理完进程立即退出 默认行为是进程终止并产生core

记住一个关键点:Java Crash走的是虚拟机内部异常分发机制,Native Crash走的是Linux内核信号投递机制。两者在系统层最终汇聚到同一个出口——debuggerd。这个问题我跟你后面讲捕获方案时会反复提到。

4.2 系统守护者:debuggerd 与 tombstoned

debuggerd是Android系统里专门处理Native Crash的守护进程。它的工作机制很有意思:系统启动时,init进程会拉起debuggerd,然后它通过socket监听来自各进程的信号通知。一旦哪个进程挂了,内核会把信号转发给debuggerd。

那tombstoned呢?它相当于debuggerd的"秘书"。debuggerd负责采集Crash现场的寄存器、堆栈、内存映射等信息,tombstoned负责把这些信息写到磁盘——也就是我们常说的tombstone文件。这些文件默认存放在/data/tombstones/目录下。

关键组件角色

  • debuggerd —— 实时attach到Crash进程,dump寄存器、callstack、maps等
  • tombstoned —— 接收dump数据,写入到/data/tombstones/tombstone_XX文件
  • logd —— 将Crash关键信息同步输出到logcat

4.3 完整链路:从崩溃到日志落盘

好,现在我们把整个链路串起来。从App发生Crash到开发者在logcat或tombstone里看到堆栈,中间经历了这些步骤:

  1. Crash触发 —— Java层抛出未捕获异常,或Native层收到信号(SIGSEGV/SIGABRT等)
  2. 内核拦截 —— 信号由Linux内核捕获,生成对应的signal事件
  3. debuggerd介入 —— 系统通知debuggerd,它attach到目标进程
  4. 现场采集 —— debuggerd dump出寄存器、PC指针、stack、memory map等
  5. tombstoned写入 —— 采集到的数据通过tombstoned写入墓碑文件
  6. logcat输出 —— 同时关键信息也会通过logd输出到logcat
Android 系统 Crash 处理流程 应用发生 Crash Java异常 / Native信号 信号 / 异常拦截 内核捕获异常信号 debuggerd 介入 系统守护进程接管 堆栈信息采集 寄存器 / 内存快照 tombstoned 写入 生成墓碑文件 开发者获取分析 解析堆栈定位问题 Java Crash 与 Native Crash 最终都会汇聚到 debuggerd / tombstoned 链路 理解全链路是设计稳健捕获方案的前提

4.4 关键源码与代码示例

如果你想深入验证这套流程,可以直接去看AOSP源码。我列出几个关键路径,你可以顺着它们把整个链路捋一遍:

  • system/core/debuggerd/ —— debuggerd 与 tombstoned 的完整实现
  • bionic/libc/ —— bionic 对 signal handler 的默认处理
  • frameworks/base/core/java/ —— Java 层的 UncaughtExceptionHandler 逻辑

下面我贴两个最基础的捕获注册代码,你一看就明白入口在哪:

Java 层注册默认异常处理器

Thread.setDefaultUncaughtExceptionHandler(
    new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            // 这里拿到最原始的异常信息
            Log.e("CrashMonitor", "Java Crash: " + t.getName(), e);
            // 可以在这里做自定义上报
        }
    }
);

Native 层注册信号处理器(精简版)

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void crash_handler(int sig, siginfo_t *info, void *context) {
    // 这里拿到信号类型和上下文
    // 可以在这里做自定义dump
    _exit(sig);
}

void setup_native_crash_handler() {
    struct sigaction sa;
    sa.sa_sigaction = crash_handler;
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
    sigemptyset(&sa.sa_mask);

    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
    sigaction(SIGFPE, &sa, NULL);
}

我的经验:Native层注册signal handler时,一定要加上SA_ONSTACK标志。我曾经在低内存设备上因为栈溢出导致handler无法执行,加上这个标志后问题就解决了。

4.5 工程实践与避坑指南

光看懂流程还不够,真正落地时你会遇到各种边界情况。我把自己踩过的坑总结一下:

⚠️ 致命陷阱:信号处理器中的死锁

在signal handler里调用malloc、printf等非异步信号安全的函数,极易触发死锁。我记得有个线上bug,就是因为handler里打印日志时持锁冲突,导致整个进程挂死,连tombstone都没生成。后来我们统一改用write()直接写fd才解决问题。

  • tombstone文件数量限制 —— 系统默认最多保留10个tombstone文件,超出后会覆盖最早的。如果你需要保存更多现场,记得自己定期备份。
  • Java Crash 和 Native Crash 的时间差 —— 有时候两个Crash会几乎同时发生,这时debuggerd可能会被前一个Crash占用,导致后一个Crash的现场采集不全。
  • 64位与32位进程的差异 —— debuggerd 会针对不同ABI使用不同的dump策略。如果你的App混合了32/64位so,记得在解析时区分处理。

我自己常用的验证方法:在开发阶段,手动触发一次SIGSEGV,然后去/data/tombstones/下看文件是否生成,内容是否完整。这个习惯帮我提前发现了好几个线上捕获方案的漏洞。

嗯,这套流程你理解了,后面的章节讲捕获方案时就会轻松很多。Android系统的Crash处理机制虽然复杂,但链路清晰,每一步都有迹可循。真正吃透它,你写出来的捕获代码才有底气。