Android Crash分类:Java层Crash、Native层Crash、ANR
做Android开发这么多年,我遇到过各种各样的崩溃。说白了,Crash不是只有一种面孔。你想想看,Java代码出错是一种死法,C++代码崩了是另一种,甚至App没崩但卡死也算一种——对,我说的就是ANR。
我把这些崩溃分成三大类:Java层Crash、Native层Crash和ANR。搞明白它们的区别,才能对症下药。下面这张图可以帮你快速建立整体认知。
Java层 Crash
先说最常见的Java层Crash。说白了,就是Java/Kotlin代码里抛出了未捕获的异常(Unchecked Exception),导致当前线程直接挂掉。如果这个线程是主线程,恭喜你,用户会看到「App已停止运行」的对话框。
我个人习惯把Java Crash分为四类高频场景:
| 异常类型 | 典型触发场景 | 我踩过的坑 |
|---|---|---|
| NullPointerException | 调用空对象的方法或属性 | 曾经在Fragment重建时忘判null,直接闪退 |
| IndexOutOfBoundsException | 访问列表越界 | RecyclerView数据更新和UI刷新不同步,下标越界 |
| ClassCastException | 强制类型转换错误 | Gson反序列化泛型擦除导致类型混淆 |
| IllegalArgumentException | 方法参数非法 | 自定义View构造方法传入了负数尺寸 |
Thread.setDefaultUncaughtExceptionHandler 全局捕获。但注意——捕获后进程仍然会被杀掉,你只能做「临终保存」操作(写日志、上报),无法阻止App退出。
Native层 Crash
Native Crash就麻烦多了。它发生在C/C++代码层,比如系统底层、第三方so库、你自己的NDK代码。崩溃时没有Java异常对象,只有信号(Signal)——比如SIGSEGV(段错误)、SIGABRT(主动触发abort)。
嗯,这里要注意:Native Crash的堆栈不经过Java层,你没法用try-catch捕获。你需要注册一个 信号处理器(Signal Handler),或者用 google-breakpad / crashpad 这样的成熟方案。
举个例子,我曾经接一个视频编辑SDK的Crash,对方只扔给我一个tombstone文件。里面全是寄存器值和内存地址,没有Java堆栈。后来我分析so库的调试符号,发现是一个回调函数里指针被释放后又被使用(Use-After-Free)。
// 注册信号处理器的核心代码(简化版)
#include <signal.h>
#include <unwind.h>
void crash_handler(int sig, siginfo_t* info, void* context) {
// 1. 获取当前堆栈
// 2. 写入文件
// 3. 调用原处理器或exit
_exit(1);
}
void install_signal_handler() {
struct sigaction sa;
sa.sa_sigaction = crash_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
}
write()、_exit() 等异步安全函数。
Native Crash的定位难度比Java高一个数量级。你需要学会看 tombstone文件,里面包含了寄存器状态、堆栈调用链、内存映射。我建议在开发阶段开启 debuggable + android:extractNativeLibs="true",这样so库会留在磁盘上,堆栈里能直接看到函数名。
ANR(Application Not Responding)
最后说说ANR。它跟Crash不一样——进程还在,但系统觉得你「没响应」了。说白了,就是主线程被阻塞了5秒以上(Input事件超时),或者广播接收器10秒内没执行完,或者服务20秒内没执行完。
ANR不会自动弹Crash对话框,但系统会写 /data/anr/traces.txt 文件,里面记录了所有线程的堆栈。我年轻时遇到一个问题:列表滑动时经常ANR,看trace发现是主线程在做数据库写入——我居然忘了用异步线程。
| ANR类型 | 超时时间 | 常见原因 |
|---|---|---|
| Input 派发超时 | 5秒 | 主线程做耗时操作(网络、文件IO、复杂计算) |
| Broadcast 超时 | 10秒 | onReceive里阻塞,或串行广播队列队列任务太多 |
| Service 超时 | 20秒 | onCreate/onStartCommand执行缓慢 |
| ContentProvider 超时 | 10秒 | provider初始化或query执行时间过长 |
Looper.getMainLooper().setMessageLogging() 打印每条Message的执行耗时,发现是一个第三方推送SDK的初始化在主线程同步读文件导致。换成异步初始化后ANR率直接降为0。
三种Crash的捕获策略对比
最后做个总结。我个人习惯把三种类型的捕获方式记成一张表:
| 类型 | 捕获手段 | 关键文件/日志 | 处理难度 |
|---|---|---|---|
| Java Crash | UncaughtExceptionHandler | Logcat + 自定义崩溃文件 | 低 |
| Native Crash | Signal Handler / Breakpad | tombstone / minidump | 高 |
| ANR | FileObserver监听trace / 自建监控 | /data/anr/traces.txt | 中 |