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 的完整原则

  1. 绝不直接在 Activity 或 Fragment 主线程调用任何 Room DAO 方法(suspend 或 Flow 除外)
  2. DAO 方法必须使用 suspend 关键字或返回 Flow/LiveData,由 Room 自动调度到后台线程
  3. 若需手动控制线程,使用 withContext(Dispatchers.IO) 包裹数据库调用
  4. 避免在 Binder 事务中传递大量数据,Room 的 @Transaction 注解也要保持事务内操作轻量
  5. 优先使用 WAL 模式提升并发读性能(Room 默认已启用 WAL)

常见陷阱:即使 DAO 方法声明为 suspend,但如果在 DAO 内部调用了其他导致主线程阻塞的同步操作(如 Thread.sleep),仍然会触发 ANR。务必保持 DAO 方法纯数据库访问,不要混合耗时逻辑。

1.6 内联流程图:主线程执行 Room 操作导致 ANR 的路径

主线程执行 Room 操作 → ANR 时序图 用户事件(点击等) 主线程处理事件 Room DAO 同步调用 SQLite 执行 + 锁竞争 主线程阻塞 >5s ANR 发生 ⚠ 风险点 ① 主线程直接调 DAO 方法 ② SQLite 写锁等待 ③ Binder 数据超限

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 执行计划。