1. 主线程缓存读写ANR根因分析
主线程 ANR 的直接表现是“应用无响应”,其根本原因是主线程的消息队列无法及时处理新消息,或处理耗时操作超过系统预设阈值(通常为 5 秒触摸/按键无响应、10 秒广播无响应)。在缓存读写的场景下,四种典型根因会单独或组合触发 ANR:主线程消息队列阻塞、I/O 耗时、锁竞争、内存分配 GC 停顿。下面对每个根因进行原理剖析和风险分析。
1.1 主线程消息队列阻塞
Android 主线程运行着一个无限循环的 Looper,不断从 MessageQueue 中取出消息并派发给对应的 Handler 处理。如果某条消息的处理耗时远超预期,后续所有消息(包括输入事件、绘制命令、生命周期回调)都会被延迟,导致应用“卡死”。当延迟累积超过 5 秒时,系统判定 ANR。
典型触发场景:在主线程的 Handler.handleMessage() 或 View.onDraw() 中直接执行同步文件读写、网络请求或复杂计算,导致 Looper 无法处理下一帧。
// ❌ 错误示例:主线程直接执行耗时文件写入
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
FileOutputStream fos = openFileOutput("cache.dat", MODE_PRIVATE);
fos.write(largeByteArray); // 阻塞 ~800ms
fos.close();
} catch (IOException e) { ... }
}
});
1.2 I/O 耗时
主线程直接进行磁盘 I/O(包括文件读写、数据库操作、SharedPreferences commit)或网络 I/O 时,系统调度、磁盘寻道、NAND 闪存磨损、文件系统锁等都会造成毫秒级甚至秒级的不可预期延迟。当 write() 或 read() 等待超过 16ms 时,屏幕掉帧;超过 5 秒则触发 ANR。
| I/O 操作类型 | 典型耗时(主线程风险等级) | ANR 触发概率 |
|---|---|---|
| SharedPreferences commit() | 10~100 ms | 中(高频调用时极高) |
| SQLite 写入(WRITE_AHEAD_LOGGING OFF) | 50~500 ms | 高 |
| FileOutputStream > 1 MB | 200 ms~2 s | 极高 |
| OkHttp 同步请求(未切换线程) | 100 ms~5 s+ | 极高 |
避坑注意:即使使用 BufferedOutputStream,底层依然会在超过缓冲区大小时触发系统调用 write(),主线程仍可能被阻塞数十毫秒。缓存读写必须使用异步 I/O 或线程池。
1.3 锁竞争
当多个线程(包括主线程)竞争同一把锁(如 synchronized 块、ReentrantLock、StampedLock)时,未能获取锁的线程会进入 BLOCKED 或 WAITING 状态。如果主线程在等待锁的过程中被挂起,而持有锁的线程在执行 I/O 或长时间计算,主线程消息队列将完全停滞,最终触发 ANR。
// ❌ 危险示例:主线程等待后台线程释放锁
final Object lock = new Object();
// 后台线程先持锁
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
Thread.sleep(6000); // 模拟长耗时 I/O
}
}
}).start();
// 主线程尝试获取同一锁
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
synchronized (lock) { // 主线程将在此阻塞 6 秒 → ANR
writeCachedData();
}
}
});
1.4 内存分配 GC 停顿
主线程频繁创建临时对象(如缓冲区、字符串、AutoBoxing 对象)会导致堆内存快速膨胀,触发 Concurrent Mark Sweep (CMS) 或 Garbage-First (G1) 垃圾回收。虽然 GC 在 ART 上已优化为并发,但 Stop-The-World (STW) 阶段(如 young GC 和 full GC)仍可能暂停主线程 5~15ms。如果 GC 发生在大对象分配或内存抖动阶段,STW 暂停可能累积超过 1 秒,叠加其他耗时后极易触发 ANR。
诊断线索:通过 Android Studio Profiler 的 Memory & CPU 录制,观察主线程时间线上是否出现频繁的黄色“GC”标记,同时 Logcat 输出 Sticky concurrent mark sweep (GC) 事件。
// ❌ 频繁分配临时对象示例(主线程每帧创建大量 ByteBuffer)
public void onDraw(Canvas canvas) {
for (int i = 0; i < 100; i++) {
byte[] temp = new byte[1024 * 1024]; // 1 MB 大对象
process(temp);
}
// 每次分配 → 垃圾回收被迫 STW
}