Android Crash 类型与捕获原理,我这些年踩过的坑
做Android开发这么多年,Crash和ANR就像两个甩不掉的影子。我记得刚入行那会儿,遇到一个线上Crash,日志里只有一句NullPointerException,堆栈还不全。当时真是抓耳挠腮,后来才明白——连异常捕获都没做好,更别提分析了。这一章咱们就来聊聊Crash的分类和底层捕获机制,算是给后面的联动监控打个地基。
Android Crash 的三种“死法”
Android系统里的Crash,说白了就三种:Java/Kotlin异常、Native崩溃、ANR。它们发生的位置、捕获方式、落盘机制都不一样。我习惯用下面这张图把它们串起来,方便记忆。
Java/Kotlin 异常:最熟悉的陌生人
这类异常包括NullPointerException、IndexOutOfBoundsException、IllegalStateException等等。它们跑在ART虚拟机里,一旦线程抛出一个没有被try-catch的异常,虚拟机会直接调用Thread#dispatchUncaughtException,最终弹到Thread.setDefaultUncaughtExceptionHandler注册的全局处理器。
ActivityThread的handleAppCrash接管,走向强制关闭对话框。所以单独设一个Handler还是不够的,需要结合Looper#setMessageLogging做二次拦截。
代码上怎么干?我习惯在Application#onCreate里尽早注册:
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
// 保存异常信息到本地文件
saveCrashToLocal(thread, throwable);
// 如果有之前的处理器,也可以继续调用
if (previousHandler != null) {
previousHandler.uncaughtException(thread, throwable);
}
});
UncaughtExceptionHandler,形成死循环导致应用彻底无响应。所以我在捕获逻辑里加了try-catch加上递归次数限制,最多处理三次就放行。
Native 崩溃:C/C++ 世界的“硬伤”
Native崩溃不像Java异常那样有堆栈打印,它直接对应Linux进程信号。比如段错误SIGSEGV(11)、非法指令SIGILL(4)、浮点异常SIGFPE(8)等。在这些信号发生时,系统内核会把进程挂起,交给debuggerd守护进程来dump tombstone文件(一般在/data/tombstones/下)。
想在App层捕获Native崩溃,主流做法是拦截信号。我早期用过Google breakpad,后来发现FileDescriptor + pipe的方式更轻量。核心思路是注册signal(SIGSEGV, myHandler),在信号处理器里fork一个子进程来写日志,避免死锁。
write()、mmap()这类系统调用。否则自己又触发SIGSEGV,那就GG了。我在一个项目中就是因为用了logcat打印,结果手机上疯狂重启,后来改用writev直接写文件才搞定。
ANR:最让人头疼的“假死”
ANR全称Application Not Responding。系统检测到主线程阻塞超过5秒(前台广播超过10秒,服务超过20秒,ContentProvider超过页面启动超时),就会弹ANR对话框。本质上,ANR不是一种“崩溃”,而是系统发送了一个SIGNAL_QUIT(3)信号,然后/data/anr/traces.txt里会记录所有线程的堆栈。
捕获ANR有两条路:
- 信号拦截法:自己捕获
SIGNAL_QUIT信号,然后主动dump当前线程堆栈。好处是实时性高,坏处是某些系统版本信号会被屏蔽。 - 监控点法:在主线程Looper里设置
setMessageLogging,记录每个消息执行时长。如果超过5秒还没回来,就认为发生了ANR。我项目里就是用的这个方法,配合FileObserver监听/data/anr/traces.txt的变更,双保险。
| 捕获方式 | 优点 | 缺点 |
|---|---|---|
| 信号拦截 | 频率准确,系统真正发信号时才触发 | 部分定制ROM拦截了SIGNAL_QUIT,兼容性差 |
| Looper监控 | 兼容好,可自定义超时阈值 | 可能漏掉因系统服务阻塞导致的ANR |
dispatchMessage前后打个时间戳,一旦超时,立刻现场dump所有线程堆栈到私有目录,避免被traces.txt覆盖。
捕获原理一句话总结
Java异常靠UncaughtExceptionHandler,Native崩溃靠信号处理器+debuggerd,ANR靠信号监听或Looper钩子。本质上都是在系统杀死进程之前,抢到一线执行机会,把现场数据写出去。你想想看,如果没抢到,那后续的监控数据就是空的,损失就大了。
我在公司内部的数据保护方案里,把这三种捕获方式封装成了一个统一的CrashCollector库,里面用LinkedBlockingQueue做生产者-消费者模型,保证写文件不阻塞主线程。具体实现,后面章节咱们再细聊。
Log.e,结果在低端机上Log系统内部又挂了,形成连环Crash。后来全改成BinaryLogBuffer直接写mmap文件,才稳定下来。