Crash 与 ANR 的基本概念
做 Android 开发这些年,我有个很深的体会:很多数据保护问题,根源其实就两种——要么是 Crash,要么是 ANR。你想想看,用户刚填完表单、刚拍完照片,突然应用闪退了,或者屏幕卡死半天,这时候数据大概率就丢了。所以搞懂这俩兄弟,是定位数据保护问题的第一步。
Crash 和 ANR 虽然都意味着应用不正常,但本质完全不同。我先用自己的话给你捋一遍:
Crash:程序直接“挂”了
Crash 就是应用进程因为未捕获的异常而终止。比如空指针、数组越界、资源没找到、运行时类型转换失败……一旦发生,系统会直接干掉你的进程。后果就是——所有未保存的数据全部消失,内存里的变量、缓存、数据库事务未提交的,统统没了。
onPause() / onStop() 可能根本来不及调用,你写的保存逻辑形同虚设。
ANR:应用“僵”了,但进程还在
ANR 全称 Application Not Responding,意思是应用超过阈值没有响应用户操作。主线程被卡住 5 秒(前台)或 10 秒(后台),系统就会弹出“应用无响应”的对话框,用户可以选“等待”或“关闭”。如果用户选关闭,同样会造成数据丢失。
常见原因:主线程里做网络请求、大量文件读写、复杂的数据库查询、或者死锁。注意,ANR 并不会强杀你的进程,但用户一旦主动关闭,你的后续保存逻辑就没机会跑了。
onPause() 里也做一次紧急保存。
一张表看清区别
| 对比项 | Crash | ANR |
|---|---|---|
| 进程状态 | 进程被系统强杀 | 进程存活,但主线程阻塞 |
| 用户感知 | 应用直接闪退 | 界面卡死,弹出提示框 |
| 根本原因 | 未捕获异常(运行时错误) | 主线程超时未响应(资源竞争/耗时操作) |
| 数据保护难度 | 无预警,来不及保存 | 用户可能主动关闭,同样来不及 |
| 日志定位 | 看 logcat 中的 Exception 堆栈 | 看 /data/anr/traces.txt 中的线程堆栈 |
说白了,Crash 是“暴力死亡”,ANR 是“窒息式卡死”。但不管哪种,最终都会威胁到用户数据。所以我们要做的第一件事就是——能区分,才能预防。
一个小例子,让你感受区别
// Crash 示例:空指针
String name = null;
if (name.equals("admin")) { // ❌ NullPointerException,直接闪退
saveData();
}
// ANR 示例:主线程 sleep
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ❌ 模拟耗时操作
try { Thread.sleep(6000); } catch (InterruptedException e) { }
// 6秒后才会执行到这里,用户早已看到 ANR 对话框
loadCriticalData();
}
你看,上面两段代码都会导致用户数据丢失——Crash 时 saveData() 根本没机会执行,ANR 时 loadCriticalData() 也可能因为用户主动关闭而中断。
我们该怎么定位?
- Crash 定位: 盯住
logcat里的FATAL EXCEPTION,看异常类型和行号。另外,第三方的崩溃收集平台(如 Firebase、Bugly)会帮你聚合堆栈,但现场信息可能不全。 - ANR 定位: 核心文件是
/data/anr/traces.txt。找到你的包名对应的堆栈,看主线程(main)在哪个方法里卡住了。特别留意Locked waiting for或blocked on等关键字。
SharedPreferences 的 getString() 上,你可能会奇怪,SP 不是很快吗?后来发现是因为有另一个线程在做 apply() 写入大量数据,导致 commit() 阻塞。所以定位 ANR 不能光看主线程,还得关注工作线程的锁持有情况。
避坑指南
我曾经犯过一个低级错误:线上出现大批量 ANR,traces 显示主线程在 View.onDraw() 中耗时。我查了代码,发现是自定义 View 里做了大量的 Canvas 绘制+文件读取。后来用 TraceView 一看,才知道是 BitmapFactory.decodeStream() 在主线程里跑。所以记住:一切可能 IO 的操作都放到子线程,尤其是 AsyncTask 已经 deprecate 了,改用 Coroutine 或 RxJava。