崩溃类型概述:ANR、Java Crash、Native Crash、OOM
说实话,很多人刚开始做性能优化时,一看到崩溃日志就头皮发麻。我当年也一样——尤其是冷启动阶段,各种崩溃乱窜,根本分不清是哪个环节出了问题。后来踩的坑多了才明白:你连对手是谁都搞不清楚,怎么打?
这一章我们就来聊聊 Android 上最常见的四种崩溃类型。说白了,就是先把敌人认全了,后面分析 Trace 才能一刀封喉。
ANR — 应用无响应
ANR 的全称是 Application Not Responding。说白了就是系统问你:「你在干啥呢?怎么半天不吭声?」
冷启动阶段的 ANR 尤其可恶——用户刚点开 App,什么都没看见,就卡住了。我记得有一次线上反馈说启动白屏后直接弹「无响应」对话框,一查发现是 ContentProvider 在 onCreate() 里做了两秒的数据库迁移。你想想看,这哪扛得住?
典型的 ANR 触发条件有三个:
- 主线程 5 秒内未处理输入事件(比如点击、触摸)
- BroadcastReceiver 在 10 秒内未执行完毕(如果是前台广播,时间更短)
- Service 前台 20 秒 / 后台 200 秒卡住
在冷启动场景下,主线程阻塞是最常见的。为什么呢?因为 Application.onCreate()、Activity.onCreate()、ContentProvider.onCreate() 这些都是在主线程执行的。如果你在这里搞网络请求、磁盘 I/O、或者等锁,主线程就「冻住」了。
实战特征:查 Trace 文件时找 "Input dispatching timed out" 或 "Waiting in main thread" 字样。另外留意 CPU usage 那一行——如果 CPU 使用率很低,大概率是线程在等锁;如果 CPU 满载,可能是计算量过大。
Java Crash — 未捕获异常
Java Crash 是最「友好」的崩溃了——因为它会在 Logcat 里给你一个完整的调用栈。从崩溃发生的线程、异常类型,到每行代码的类名、行号,清清楚楚。
常见的 Java Crash:
NullPointerException— 我最常遇到的就是 findViewById 返回 null 还接着用IndexOutOfBoundsException— 列表操作没判边界ClassCastException— 把对象强转成了错误类型IllegalStateException— Fragment 在 commit 后还在操作
冷启动时特别容易在 Activity.onCreate() 里出现空指针——比如从 Intent 取 extra 但没做 null 判断。嗯,这里要提醒一下:别相信任何人传给你的 Intent 数据。
我的习惯:线上收集到 Java Crash 日志后,先看异常类型,再看堆栈第一行——往往是真正出错的代码。别被中间那些系统框架层带偏了,除非是系统 Bug。
Native Crash — C/C++ 层崩溃
Native Crash 比 Java Crash 难搞多了。你没有堆栈里的行号,只有一段内存地址和寄存器信息。为什么会这样?因为 C/C++ 编译后是二进制指令,没有保留源代码行号映射(除非你放了 symbols)。
常见的信号:
- SIGSEGV (信号 11) — 段错误,访问了非法内存。比如空指针解引用、已经释放的内存再读写。
- SIGABRT (信号 6) — 调用了 abort(),通常是 assert 失败或者 libc 检测到堆损坏。
- SIGFPE (信号 8) — 浮点异常,比如除零。
- SIGILL (信号 4) — 非法指令,代码执行到了不该去的地方。
冷启动阶段的 Native Crash,我印象最深的一次是第三方 SDK 的 so 文件在加载时触发了段错误——因为.so 的 target API level 和设备的系统库版本不兼容。查了半天,最后用 readelf -d 看依赖库才找到问题。
我曾经踩过的坑:千万别光看 crash address 就以为能直接定位。先确认 /proc/self/maps 里 so 的加载基址,然后做地址偏移计算。用 ndk-stack 或者 addr2line 转换成具体文件和行号。如果你不带 .sym 符号表到线上,那就只能靠反汇编猜了——后者非常痛苦。
OOM — 内存不足
OOM 全称 Out Of Memory。在 Android 上分两种情况:
- Java Heap OOM — 堆内存分配超过了 dalvik/art 虚拟机的最大限制。日志里会打印
java.lang.OutOfMemoryError,并附带当前堆大小信息。 - Native Heap OOM — 通过 malloc / mmap 分配的内存太多,系统内存不足。这种情况不会抛 Java 异常,通常会导致进程被 LMK (Low Memory Killer) 杀掉,或者出现 SIGSEGV。
冷启动时的 OOM 特别「坑爹」。为什么?因为这时候很多资源在集中初始化——图片加载、布局 inflate、数据库连接池、多线程池……如果你不做节制,内存瞬间就爆了。我记得有一次冷启动阶段同时 decode 了 50 张大图,结果 8 秒内直接 OOM——用户刚看到闪屏就崩了。
典型的 OOM 堆栈会显示 OutOfMemoryError: Failed to allocate a 29303836 byte allocation with 12288500 free bytes。这时候你就知道,某个地方申请了 29MB 的大内存,但堆里只剩 12MB 了。
| OOM 子类型 | 日志特征 | 冷启动典型案例 |
|---|---|---|
| Java Heap OOM | java.lang.OutOfMemoryError + 分配大小 |
启动时一次加载多张大图、大量 Bitmap 未复用 |
| Native Heap OOM | 无 Java 异常,进程突然消失;logcat 可见 lowmemorykiller |
大量 mmap 加载 so 或模型文件;JNI 层分配未释放 |
我自己的定位方法:收到 Java OOM 后,先上 MAT 或 Android Studio Profiler 看 Dominator Tree,哪个对象占据的内存最大,源头在哪。如果是 Native OOM,就得用 malloc hook 或者 adb shell dumpsys meminfo 逐个排查。简单粗暴的做法:先怀疑图片——因为大部分冷启动 OOM 都是 Bitmap 干的。
四种崩溃的 Trace 差异对比
好了,关键问题来了:拿到一份 trace 文件(比如 /data/anr/traces.txt 或 bugreport 里的),你怎么一眼分辨是哪种崩溃?
| 崩溃类型 | Trace 开头特征 | 关键线索 |
|---|---|---|
| ANR | ANR in com.xxx + reason: Input dispatching timed out |
主线程堆栈 + CPU usage 部分 |
| Java Crash | FATAL EXCEPTION: main 或 FATAL EXCEPTION: AsyncTask #1 |
异常类型和完整 Java 堆栈 |
| Native Crash | *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 分隔符 + pid: xxx, signal 11 |
信号值 + backtrace 段 + 寄存器内容 |
| OOM | 通常不在 ANR trace 里,而是在 logcat 或者 bugreport 的 dumpstate 中 |
OutOfMemoryError 或 lowmemorykiller 的 kill 记录 |
这里有个小技巧:如果你在系统 trace 里看到某个线程长时间处于 WAIT 或 MONITOR 状态,而且主线程也在等它,那往往是 ANR 的源头;如果你看到 signal 11,别慌——先查 /proc/self/maps 找出错的 so 是谁家的。