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 里操作。
怎么用?
- 运行你的 App(Debug 模式)
- 打开 Android Studio 底部的「App Inspection」标签
- 选择「Database Inspector」
- 你会看到当前进程里所有的 Room 数据库
你可以:
- 实时查看表数据——App 里增删改了,这边立刻刷新
- 直接修改数据——双击单元格就能改,改完点「Apply」就生效
- 执行自定义查询——写个 SELECT 语句,看看结果对不对
- 导出数据库文件——右键就能把 .db 文件导出来,用 SQLite 工具分析
我个人最常用的功能:在调试 UI 时,直接通过 Database Inspector 改数据库里的某个字段值,然后切回 App 看界面变化。省去了「改代码 → 重新编译 → 跑起来 → 等数据加载」这一套流程,效率提升很明显。
不过要注意一点:Database Inspector 只能调试你当前连接的进程。如果你的 App 有多个进程(比如用到了多进程的 ContentProvider),那只能看到主进程的数据库。
25.5 知识体系总览
下面这张图,把本章的核心内容串起来了。你可以把它当作一个「调试检查清单」来用。
这四个工具和方法,我建议你从项目一开始就集成进去。别等到线上出了 Bug 再回头补——那时候你连问题复现都困难。
我的最终建议:
- 小项目:Database Inspector + 日志回调,够用了
- 中大型项目:加上 Stetho(Debug 模式)和统一异常封装
- 团队协作:把日志格式和异常处理规范写进项目的 README 里
好了,这一章的内容就到这里。错误处理和日志这东西,平时你可能觉得「没啥技术含量」,但真正遇到线上事故时,它就是你的救命稻草。希望你能把这些实践用起来,别像我当年那样,对着一个空指针查一下午。
公众号:蓝海资料掘金营,微信deep3321