性能问题概述:常见性能瓶颈与开发陷阱
正经做 Room 开发,没人敢说自己从来没遇到过卡顿。我记得有一次,用户反馈列表滑动掉帧,打 log 一看,主线程执行了 300 多次数据库查询——每个 item 都去读了一次 DB。嗯,今天我们就来聊聊,Room 性能到底容易坏在哪。
一、常见性能瓶颈
我把日常开发里最常碰到的三刀「软肋」总结了一下:
| 瓶颈类型 | 典型表现 | 根因 |
|---|---|---|
| 查询慢 | 列表加载要等半秒甚至一秒 | 没走索引、关联表太多、返回数据量大 |
| 事务冲突 / 写放大 | 多个线程同时写 DB,提示“database is locked” | 未合理控制并发粒度,批量操作未合并事务 |
| 主线程操作 | ANR 或 UI 卡顿 | Room 默认不允许主线程查询,有的人强行用 allowMainThreadQueries() 或者用了同步查 |
我早期做过一个笔记 App,用户写一条笔记就触发一次批量标签关联插入,每插入一次启动一个独立事务。结果用户快速记多条笔记时,DB 直接锁住十几秒。后来把插入改成@Transaction包裹的批量insertAll,问题瞬间消失。
二、开发中常见的性能陷阱
这些坑我几乎在每次 Code Review 里都能逮到,今天帮你一次性排雷。
1. 索引缺失
Room 不会自动给每个字段加索引。如果你经常按某个字段 WHERE 或 ORDER BY,一定要显式声明 @Index。
// 错误示范:没索引,查 10 万条记录全表扫
@Entity
data class User(
@PrimaryKey val id: Int,
val email: String
)
// 正确做法:加索引
@Entity(indices = [Index(value = ["email"], unique = true)])
data class User(
@PrimaryKey val id: Int,
val email: String
)
2. 没开 WAL 模式
Room 默认使用 journal mode = DELETE,读写互相阻塞。你要是想同时读和写,建议开启 WAL。
// 在 Room.databaseBuilder 中开启
Room.databaseBuilder(context, AppDatabase::class.java, "mydb")
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)
.build()
我个人的习惯是:只要有并发读写场景,第一时间就开 WAL。尤其像 IM 聊天、实时笔记这种 App,不开 WAL 基本就要被吐槽“发完消息列表不刷新”。
3. 返回了整个对象,但只需要几个字段
很多人图省事,DAO 里直接 @Query("SELECT * FROM…") 返回一个完整对象。结果 Room 把全部字段映射了,还占用了序列化/反序列化的时间。
// 坏味道:明明只需要 name 和 avatar
@Query("SELECT * FROM user")
fun getAllUsers(): List<User>
// 好做法:只查需要的字段
@Query("SELECT name, avatar FROM user")
fun getUserSimple(): List<UserSimple>
你想想看,一个 User 表如果有十几个字段,你每取一条就要把全部字段读一遍,内存和 IO 都浪费。所以说,能用 POJO 就别用 Entity。
4. 在主线程做批量操作
Room 默认不支持主线程 query,但是有些人为了省事,用 allowMainThreadQueries() 在 UI 线程里执行 insertAll(list)。遇到大量数据插入,UI 直接冻住。
Coroutine 的 withContext(Dispatchers.IO) 或 RxJava 的 Schedulers.io()。如果你非要问用哪个,我推荐协程。
5. 事务没合并
很多人写批量插入时,循环调用 DAO 方法,每个方法默认开一个事务。比如:
// 错误:每条 insert 独立事务
for (item in list) {
dao.insert(item)
}
// 正确:用 @Transaction 包裹一次写入
@Transaction
fun insertAll(items: List<Item>) {
for (item in items) {
dao.insert(item)
}
}
这种循环写入,在外层加上 @Transaction 后,性能提升可能是 10 倍级别的。我记得之前有个用户反馈“保存 50 条记录要 8 秒”,改成合并事务后降到了 0.3 秒。
三、快速定位指南
当你怀疑 Room 有性能问题时,我建议按这个顺序排查:
- 确认是否在主线程 —— 开 StrictMode 或者看
Thread.currentThread()的名字 - 检查有无慢查询 —— 给 Room 启用
setQueryExecutor打日志 - 确认有无锁争用 —— 用 Android Studio 的 Database Inspector 看事务耗时
- 检查索引覆盖率 —— 导出
EXPLAIN QUERY PLAN的输出,看看是不是全表扫