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里看到堆栈,中间经历了这些步骤:
- Crash触发 —— Java层抛出未捕获异常,或Native层收到信号(SIGSEGV/SIGABRT等)
- 内核拦截 —— 信号由Linux内核捕获,生成对应的signal事件
- debuggerd介入 —— 系统通知debuggerd,它attach到目标进程
- 现场采集 —— debuggerd dump出寄存器、PC指针、stack、memory map等
- tombstoned写入 —— 采集到的数据通过tombstoned写入墓碑文件
- logcat输出 —— 同时关键信息也会通过logd输出到logcat
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处理机制虽然复杂,但链路清晰,每一步都有迹可循。真正吃透它,你写出来的捕获代码才有底气。