Crash 与 ANR 的基本概念

做 Android 开发这些年,我有个很深的体会:很多数据保护问题,根源其实就两种——要么是 Crash,要么是 ANR。你想想看,用户刚填完表单、刚拍完照片,突然应用闪退了,或者屏幕卡死半天,这时候数据大概率就丢了。所以搞懂这俩兄弟,是定位数据保护问题的第一步。

Crash 和 ANR 虽然都意味着应用不正常,但本质完全不同。我先用自己的话给你捋一遍:

Crash:程序直接“挂”了

Crash 就是应用进程因为未捕获的异常而终止。比如空指针、数组越界、资源没找到、运行时类型转换失败……一旦发生,系统会直接干掉你的进程。后果就是——所有未保存的数据全部消失,内存里的变量、缓存、数据库事务未提交的,统统没了。

⚠️ 数据保护风险: Crash 发生时,onPause() / onStop() 可能根本来不及调用,你写的保存逻辑形同虚设。

ANR:应用“僵”了,但进程还在

ANR 全称 Application Not Responding,意思是应用超过阈值没有响应用户操作。主线程被卡住 5 秒(前台)或 10 秒(后台),系统就会弹出“应用无响应”的对话框,用户可以选“等待”或“关闭”。如果用户选关闭,同样会造成数据丢失。

常见原因:主线程里做网络请求、大量文件读写、复杂的数据库查询、或者死锁。注意,ANR 并不会强杀你的进程,但用户一旦主动关闭,你的后续保存逻辑就没机会跑了。

💡 我在项目中遇到过:一个 ANR 发生在用户点击“保存”按钮时,主线程正在做加密操作,耗时太久。用户等不及点了“关闭”,结果那张刚拍的照片没存到本地。后来我们改用异步加密,同时在 onPause() 里也做一次紧急保存。

一张表看清区别

对比项CrashANR
进程状态进程被系统强杀进程存活,但主线程阻塞
用户感知应用直接闪退界面卡死,弹出提示框
根本原因未捕获异常(运行时错误)主线程超时未响应(资源竞争/耗时操作)
数据保护难度无预警,来不及保存用户可能主动关闭,同样来不及
日志定位看 logcat 中的 Exception 堆栈看 /data/anr/traces.txt 中的线程堆栈

说白了,Crash 是“暴力死亡”,ANR 是“窒息式卡死”。但不管哪种,最终都会威胁到用户数据。所以我们要做的第一件事就是——能区分,才能预防。

Crash vs ANR · 知识体系图 数据保护风险 Crash(程序崩溃) 未捕获异常 进程被强杀 ANR(应用无响应) 主线程超时 用户主动关闭 定位技巧(日志 + traces) 💡 牢记:Crash 看堆栈,ANR 看 traces.txt,数据保护要双管齐下

一个小例子,让你感受区别

// 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() 也可能因为用户主动关闭而中断。

我们该怎么定位?

  1. Crash 定位: 盯住 logcat 里的 FATAL EXCEPTION,看异常类型和行号。另外,第三方的崩溃收集平台(如 Firebase、Bugly)会帮你聚合堆栈,但现场信息可能不全。
  2. ANR 定位: 核心文件是 /data/anr/traces.txt。找到你的包名对应的堆栈,看主线程(main)在哪个方法里卡住了。特别留意 Locked waiting for 或 blocked on 等关键字。
🔥 个人经验: 我在线上遇到过一次非常隐蔽的 ANR。主线程卡在 SharedPreferences 的 getString() 上,你可能会奇怪,SP 不是很快吗?后来发现是因为有另一个线程在做 apply() 写入大量数据,导致 commit() 阻塞。所以定位 ANR 不能光看主线程,还得关注工作线程的锁持有情况。

避坑指南

我曾经犯过一个低级错误:线上出现大批量 ANR,traces 显示主线程在 View.onDraw() 中耗时。我查了代码,发现是自定义 View 里做了大量的 Canvas 绘制+文件读取。后来用 TraceView 一看,才知道是 BitmapFactory.decodeStream() 在主线程里跑。所以记住:一切可能 IO 的操作都放到子线程,尤其是 AsyncTask 已经 deprecate 了,改用 Coroutine 或 RxJava。