29、性能监控:慢查询检测、数据库大小监控、ANR预防、Profiler使用
说实话,Room 用起来确实爽,写个 DAO 接口、定义个 Entity,CRUD 就全有了。但爽归爽,性能问题该来还是会来。我见过不少项目,上线后用户反馈「卡死了」「点一下等半天」,最后定位到是数据库慢查询导致的。
所以这一章,咱们就聊聊怎么给 Room 做「体检」。说白了,就是四个维度:慢查询检测、数据库大小监控、ANR 预防、Profiler 使用。掌握了这些,你才算真正会用 Room。
29.1 慢查询检测:别让 SQL 拖垮你的 UI
慢查询,就是执行时间超过预期的 SQL 语句。在 Room 里,默认的 SQLite 执行速度其实不慢,但一旦数据量上来,或者查询写得不好,问题就暴露了。
我自己的习惯是,在开发阶段就开启 Room 的查询日志。怎么开?很简单:
// 在构建 Room 数据库时,开启查询日志
val db = Room.databaseBuilder(
context,
AppDatabase::class.java,
"my_database.db"
)
.allowMainThreadQueries() // 仅用于调试,别上线
.setQueryCallback(object : RoomDatabase.QueryCallback {
override fun onQuery(sql: String, bindArgs: List<Any?>) {
Log.d("RoomMonitor", "SQL: $sql, Args: $bindArgs")
}
})
.build()
这样每条 SQL 都会打印出来。但光打印还不够,你得知道哪条慢。我建议配合 SlowQueryDetector 来用:
class SlowQueryDetector(private val thresholdMs: Long = 200) : RoomDatabase.QueryCallback {
private val queryTimes = mutableMapOf<String, Long>()
override fun onQuery(sql: String, bindArgs: List<Any?>) {
val start = System.currentTimeMillis()
// 这里可以结合 AOP 或自定义 DAO 来记录结束时间
// 简单做法:在 DAO 方法前后手动计时
}
fun reportSlowQuery(sql: String, durationMs: Long) {
if (durationMs > thresholdMs) {
Log.w("SlowQuery", "慢查询: $sql, 耗时: ${durationMs}ms")
// 可以上报到后台
}
}
}
29.2 数据库大小监控:别让 DB 变成「胖子」
数据库文件太大,也会拖慢性能。你想想看,一个 100MB 的数据库,打开都要好几秒。我遇到过最夸张的一次,有个社交 App 的数据库涨到了 800MB,用户手机直接卡死。
监控数据库大小,其实很简单。Room 的数据库文件存在 context.getDatabasePath("xxx.db") 里。你可以定期检查:
fun getDatabaseSize(context: Context, dbName: String): Long {
val dbFile = context.getDatabasePath(dbName)
return if (dbFile.exists()) dbFile.length() else 0L
}
// 使用示例
val size = getDatabaseSize(context, "my_database.db")
Log.d("DBMonitor", "数据库大小: ${size / 1024 / 1024} MB")
我建议在 App 启动时、或者每次数据库写入操作后,都检查一下大小。如果发现异常增长,就要排查是不是有缓存表没清理,或者日志表无限膨胀。
VACUUM 来压缩。
29.3 ANR 预防:别让 Room 成为「罪魁祸首」
ANR(Application Not Responding)是 Android 开发者的噩梦。而 Room 导致 ANR 的常见原因就一个:在主线程执行了耗时数据库操作。
Room 默认会检查你是否在主线程执行数据库操作,如果检测到,会直接抛异常。但有些人为了省事,调用了 allowMainThreadQueries(),这就埋下了隐患。
我自己的原则是:所有数据库操作都走协程。Room 从 2.1 版本开始就原生支持协程,用起来非常丝滑:
// DAO 中定义挂起函数
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?
}
// ViewModel 中调用
class UserViewModel(private val userDao: UserDao) : ViewModel() {
fun loadUser(userId: Long) {
viewModelScope.launch {
val user = withContext(Dispatchers.IO) {
userDao.getUserById(userId)
}
// 更新 UI
}
}
}
但光用协程还不够。有时候你明明在 IO 线程执行,但查询太慢,导致协程挂起时间过长,用户操作还是卡。这时候就需要结合慢查询检测来优化。
Flow 监听数据库变化,结果在 UI 线程上做了大量数据转换,导致 ANR。记住:Flow 的收集操作默认在协程上下文中执行,如果你在 collect 里做耗时操作,记得切线程。
29.4 Profiler 使用:用工具说话
手动打日志虽然有用,但真正定位性能瓶颈,还得靠工具。Android Studio 自带的 Profiler 就是最好的帮手。
怎么用?我一般分三步:
- 打开 Profiler:在 Android Studio 底部工具栏点击「Profiler」,或者用快捷键
Ctrl+Shift+A搜索。 - 选择数据库监控:在 Profiler 面板里,选择「Database」标签。这里会显示所有数据库操作的时间线。
- 分析慢查询:点击某个查询,可以看到详细的 SQL 语句、执行时间、绑定参数。如果发现某个查询执行时间特别长,那就是优化目标。
Profiler 还能看到数据库连接数、事务数等指标。我习惯在测试场景下跑一遍核心流程,然后看 Profiler 的数据库面板,找出那些「异常尖峰」。
29.5 知识体系总览
下面这张图,把本章的核心内容串起来了。你可以把它当作一个检查清单:
29.6 实战建议:建立监控闭环
光有工具还不够,你得形成一套闭环流程。我自己的做法是:
- 开发阶段:开启慢查询日志 + Profiler 实时监控,发现慢查询立即优化。
- 测试阶段:用自动化脚本跑核心场景,收集数据库大小和查询耗时数据。
- 上线后:通过埋点上报慢查询和数据库大小,设置告警阈值。
举个例子,我曾在线上环境发现某个查询平均耗时 800ms,排查后发现是 LIKE '%keyword%' 导致的。改成全文索引后,耗时降到 50ms。如果没有监控,这个问题可能永远发现不了。
公众号:蓝海资料掘金营,微信deep3321