25、错误处理与日志:异常捕获、日志记录、Stetho调试、Database Inspector

说实话,Room 开发中真正让人头疼的,往往不是 SQL 写不对,而是——出了问题你完全不知道错在哪

我自己早期做项目时,就吃过这个亏。数据库操作报了个空指针,Logcat 里只看到一行「null cannot be cast to non-null type」,查了整整一下午,最后发现是 DAO 返回的字段名跟实体类对不上。嗯,从那以后,我养成了一个习惯:先把错误处理和日志体系搭好,再写业务代码

这一章,我们就来聊聊怎么让 Room 的「黑盒」变成「透明」的。

25.1 异常捕获:别让崩溃静悄悄

Room 的异常主要分两类:编译期异常运行时异常

编译期异常,比如 SQL 语法写错了,Room 会在编译时直接报错,这反而是好事——你还没跑起来就发现了。真正要命的,是运行时异常。

最常见的运行时异常场景:

  • 数据库迁移失败(Migration 没写对)
  • 并发写入冲突(多线程同时操作同一个表)
  • 类型转换错误(比如把 String 存成了 Long)
  • 外键约束违反(删了父表记录,子表还有引用)

我的做法是:在 Repository 层统一捕获异常,然后转成业务层能理解的错误类型。别让 SQLiteException 直接飞到 UI 层去。

// 统一异常处理封装
sealed class DatabaseResult<out T> {
    data class Success<out T>(val data: T) : DatabaseResult<T>()
    data class Error(val exception: Throwable, val message: String = "") : DatabaseResult<Nothing>()
}

suspend fun <T> safeDatabaseCall(call: suspend () -> T): DatabaseResult<T> {
    return try {
        DatabaseResult.Success(call.invoke())
    } catch (e: SQLiteConstraintException) {
        Log.e("DB_ERROR", "约束违反: ${e.message}")
        DatabaseResult.Error(e, "数据冲突,请稍后重试")
    } catch (e: SQLiteException) {
        Log.e("DB_ERROR", "SQLite异常: ${e.message}")
        DatabaseResult.Error(e, "数据库操作失败")
    } catch (e: Exception) {
        Log.e("DB_ERROR", "未知异常: ${e.message}")
        DatabaseResult.Error(e, "系统繁忙")
    }
}

个人小技巧:我在每个 DAO 方法里都加了一个「兜底」的 try-catch,但不会在 DAO 里处理——而是抛出一个自定义的业务异常,让上层统一处理。这样代码干净,排查也方便。

25.2 日志记录:把 Room 的「悄悄话」打印出来

Room 默认是不打印 SQL 语句的。你想想看,你写了个查询,它到底生成了什么 SQL?参数传对了没?这些信息默认都藏在底层。

要打开 Room 的日志,其实就一行配置:

val database = Room.databaseBuilder(
    context,
    AppDatabase::class.java,
    "my_database.db"
)
.allowMainThreadQueries() // 仅调试用,生产环境别开
.setQueryCallback(object : RoomDatabase.QueryCallback {
    override fun onQuery(sql: String, bindArgs: List<Any?>) {
        Log.d("ROOM_SQL", "SQL: $sql")
        Log.d("ROOM_SQL", "参数: $bindArgs")
    }
})
.build()

我个人习惯在 Debug 构建类型下开启这个回调,Release 里关掉。因为生产环境打印 SQL 会有性能开销,而且可能泄露敏感数据。

日志分级建议:

级别 用途 示例
VERBOSE 详细的 SQL 语句和参数 SELECT * FROM users WHERE id = ?
DEBUG 数据库操作流程 开始事务、提交事务
INFO 关键节点信息 数据库版本升级成功
WARN 潜在问题 查询返回空结果集
ERROR 异常和错误 迁移失败、约束违反

另外,我建议你给每个 DAO 方法加一个「入口日志」和「出口日志」。比如:

@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?

// 在 Repository 里调用时
suspend fun loadUser(userId: Long): DatabaseResult<User> {
    Log.d("USER_REPO", "开始查询用户: $userId")
    val result = safeDatabaseCall { userDao.getUserById(userId) }
    Log.d("USER_REPO", "查询结果: $result")
    return result
}

这样做的好处是:你翻 Logcat 时,一眼就能看出哪个操作耗时、哪个操作失败了。我曾经靠这个定位过一个诡异的「偶发查询超时」问题——最后发现是某个第三方库在后台频繁写同一个表,导致锁冲突。

25.3 Stetho 调试:像看网页一样看数据库

Stetho 是 Facebook 开源的一个调试工具。说白了,它让你能在 Chrome 浏览器里直接查看 App 的数据库、网络请求、SharedPreferences 等内容。

配置起来很简单:

// build.gradle (app 模块)
debugImplementation 'com.facebook.stetho:stetho:1.6.0'

// Application 的 onCreate 里初始化
Stetho.initializeWithDefaults(this)

然后你跑起 App,在 Chrome 地址栏输入 chrome://inspect,就能看到你的设备了。点击「inspect」,切换到「Resources」标签,就能看到数据库表结构和数据。

我常用的 Stetho 场景:

  • 快速查看某张表里到底有几条数据
  • 手动执行一条 SQL 来验证查询逻辑
  • 对比两个版本数据库的字段差异

注意:Stetho 只应该在 Debug 构建中使用。Release 包如果带了 Stetho,等于把你的数据库暴露给任何能连上你设备的人。我曾经见过有人把 Stetho 带到生产环境,结果用户数据被第三方工具抓了个遍……嗯,别犯这种错。

25.4 Database Inspector:Android Studio 自带的「瑞士军刀」

从 Android Studio 4.1 开始,Google 内置了 Database Inspector。说实话,这工具比 Stetho 好用多了——不需要额外依赖,不需要 Chrome,直接在 IDE 里操作

怎么用?

  1. 运行你的 App(Debug 模式)
  2. 打开 Android Studio 底部的「App Inspection」标签
  3. 选择「Database Inspector」
  4. 你会看到当前进程里所有的 Room 数据库

你可以:

  • 实时查看表数据——App 里增删改了,这边立刻刷新
  • 直接修改数据——双击单元格就能改,改完点「Apply」就生效
  • 执行自定义查询——写个 SELECT 语句,看看结果对不对
  • 导出数据库文件——右键就能把 .db 文件导出来,用 SQLite 工具分析

我个人最常用的功能:在调试 UI 时,直接通过 Database Inspector 改数据库里的某个字段值,然后切回 App 看界面变化。省去了「改代码 → 重新编译 → 跑起来 → 等数据加载」这一套流程,效率提升很明显。

不过要注意一点:Database Inspector 只能调试你当前连接的进程。如果你的 App 有多个进程(比如用到了多进程的 ContentProvider),那只能看到主进程的数据库。

25.5 知识体系总览

下面这张图,把本章的核心内容串起来了。你可以把它当作一个「调试检查清单」来用。

Room 错误处理与调试体系 异常捕获 • 统一异常封装 (DatabaseResult) • Repository 层统一 try-catch • 区分 SQLiteConstraintException • 避免异常直接飞到 UI 层 日志记录 • setQueryCallback 打印 SQL • Debug 开启,Release 关闭 • 入口/出口日志标记 • 分级日志 (V/D/I/W/E) Stetho 调试 • Chrome inspect 查看数据库 • 手动执行 SQL 验证 • 对比数据库版本差异 • ⚠️ 仅限 Debug 构建 Database Inspector • 实时查看/修改表数据 • 执行自定义查询 • 导出 .db 文件分析 • 仅限当前进程 目标:让数据库操作「可观测、可追踪、可复现」

这四个工具和方法,我建议你从项目一开始就集成进去。别等到线上出了 Bug 再回头补——那时候你连问题复现都困难。

我的最终建议:

  • 小项目:Database Inspector + 日志回调,够用了
  • 中大型项目:加上 Stetho(Debug 模式)和统一异常封装
  • 团队协作:把日志格式和异常处理规范写进项目的 README 里

好了,这一章的内容就到这里。错误处理和日志这东西,平时你可能觉得「没啥技术含量」,但真正遇到线上事故时,它就是你的救命稻草。希望你能把这些实践用起来,别像我当年那样,对着一个空指针查一下午。


公众号:蓝海资料掘金营,微信deep3321