1. ANR 根因与 Room 主线程风险
1.1 ANR 触发机制概述
Android 系统通过 Input 事件超时(5 秒)、Broadcast 超时(10 秒) 和 Service 前台超时(20 秒) 检测主线程是否被阻塞。一旦主线程超过对应阈值仍未响应,系统会弹出 ANR 对话框并记录 traces.txt。
Room 作为 Android 官方的 ORM 框架,默认在主线程执行的任何数据库操作——包括查询、插入、更新、删除——都会直接导致主线程繁忙,极易触发 ANR。
1.2 三大直接风险
| 风险类型 | 触发场景 | 后果 |
|---|---|---|
| 主线程耗时数据库操作 | 直接在主线程调用 dao.query() 或 dao.insert() |
主线程被阻塞,超过5秒即 ANR |
| SQLite 锁竞争 | 多线程并发写操作(默认 WAL 模式下写操作串行),长时间持锁 | 主线程等待锁释放,导致 ANR |
| TransactionTooLargeException | Binder 事务传递超过 1MB 数据(如 Room 返回大量结果、Cursor 跨进程传递) | 主线程崩溃或 ANR(若在写入或回调中发生) |
1.3 错误示例:直接在主线程查询
// 错误做法:Activity 中直接调用 Room DAO
class MainActivity : AppCompatActivity() {
private val userDao = AppDatabase.getInstance(this).userDao()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 以下代码在 UI 线程执行数据库查询
val users: List<User> = userDao.getAllUsers() // 可能耗时 >5 秒
// 更新 UI...
}
}
1.4 Room 与 Flow 的正确配合框架
Room 从 2.1.0 开始原生支持返回 Flow<T> 类型,配合 Kotlin 协程的 Dispatchers.IO 可自动将数据库操作移至后台线程,观察者(UI 层)在 Dispatchers.Main 安全收集,彻底避免主线程阻塞。
关键 API 与配置
- DAO 方法返回
Flow<List<Entity>>或Flow<Entity?> - 使用
room-ktx依赖(自动处理协程) - 收集 Flow 时使用
lifecycleScope.launchWhenStarted或repeatOnLifecycle
// 正确的 DAO 定义 - 返回 Flow
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>> // 自动在 Dispatchers.IO 执行
}
// Activity 中安全收集
class MainActivity : AppCompatActivity() {
private val userDao = AppDatabase.getInstance(this).userDao()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
userDao.getAllUsers().collect { users ->
// 此回调在 Main 线程,不会阻塞
updateUI(users)
}
}
}
}
}
1.5 避免 ANR 的完整原则
- 绝不直接在
Activity或Fragment主线程调用任何 Room DAO 方法(suspend或Flow除外) - DAO 方法必须使用
suspend关键字或返回Flow/LiveData,由 Room 自动调度到后台线程 - 若需手动控制线程,使用
withContext(Dispatchers.IO)包裹数据库调用 - 避免在 Binder 事务中传递大量数据,Room 的
@Transaction注解也要保持事务内操作轻量 - 优先使用 WAL 模式提升并发读性能(Room 默认已启用 WAL)
常见陷阱:即使 DAO 方法声明为 suspend,但如果在 DAO 内部调用了其他导致主线程阻塞的同步操作(如 Thread.sleep),仍然会触发 ANR。务必保持 DAO 方法纯数据库访问,不要混合耗时逻辑。
1.6 内联流程图:主线程执行 Room 操作导致 ANR 的路径
1.7 调试与诊断方法
- 抓取
traces.txt:查看主线程堆栈中是否存在Room.database、SQLiteSession相关调用 - 使用
adb shell dumpsys dropbox --print data_app_anr获取 ANR 详情 - Room 的
allowMainThreadQueries()仅为测试用,线上绝不可开启
// 调试时检查主线程是否执行数据库操作
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
if (thread.name == "main" && throwable is SQLiteException) {
Log.e("ANR_CHECK", "主线程数据库异常", throwable)
}
}
性能基线:确保单次数据库查询在非主线程下耗时 < 200ms;若超过 1s 应检查索引或数据量。通过 Room 的 @RawQuery 配合 EXPLAIN QUERY PLAN 分析 SQL 执行计划。