Room 的预填充数据库(pre-populated database)如何实现?
解读
国内面试中,这道题常被用来区分“只会写增删改查”与“真正做过存量数据迁移/离线包”的候选人。
面试官想确认三点:
- 是否知道 Room 2.2.0+ 官方提供的
createFromAsset()/createFromFile()API,而不是自己手动 copy; - 是否理解预填充的时机(数据库首次创建时,且与当前版本号绑定),以及升级场景下如何“再填充”或“跳过”;
- 是否踩过国产 ROM 的坑:assets 大于 1 GB 无法压缩、微信/QQ 清理缓存导致文件丢失、多进程同时触发预填充造成 SQLiteBusyException 等。
回答时先给“官方正统方案”,再补“国内实战踩坑”,容易拿到高分。
知识点
- RoomDatabase.Builder 的 createFromAsset(String assetPath)、createFromFile(File)、createFromInputStream(Supplier<InputStream>) 三个重载。
- 预填充只发生在“数据库文件不存在”且“最终版本号一致”时;若数据库已存在,Room 直接打开,不会再次覆盖。
- 若后续升级需要“再灌入”新数据,应结合 destructiveMigration 或自定义 Migration,在升级脚本里用
INSERT OR REPLACE INTO … SELECT … FROM new_table方式合并,而不是重新 createFromAsset。 - 国内渠道包常把城市字典、离线地图等 100 MB+ 的 db 放在 assets,此时需在
build.gradle中关闭压缩:aaptOptions { noCompress 'db' },否则系统会二次解压导致首次启动耗时 3-5 s。 - 多进程场景(推送、下载、主进程同时启动 Room)要在
RoomDatabase.Builder上加.enableMultiInstanceInvalidation()并保证预填充文件只 copy 一次,否则容易报 SQLiteDatabaseLockedException。 - 合规角度,预填充数据若含用户隐私(如手机号段归属),需随隐私政策声明来源与用途,否则国内应用市场审核会被驳回。
答案
官方方案(Kotlin 示例):
- 将预置数据库 city.db 放到
src/main/assets/databases/city.db。 - 在 Room 构建时调用:
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_city.db"
)
.createFromAsset("databases/city.db") // 关键一行
.fallbackToDestructiveMigration() // 开发阶段方便,上线后改用 Migration
.build()
- 首次访问数据库时,Room 会在 IO 线程把 assets 里文件拷贝到
/databases/目录,再打开;拷贝完成前,RoomDatabase.getOpenHelper().readableDatabase会阻塞,因此建议结合RoomDatabase.Callback在onCreate回调里再插入业务默认值,保证事务一致性。
升级场景:
若 city.db 后续新增字段,需把新 db 命名为 city_v2.db,放在 assets,同时在 Migration 中把旧表数据迁移到新结构,而不是再次 createFromAsset,否则用户本地数据会被覆盖。
国内踩坑补充:
- assets 文件大于 1 GB 时,部分国产 ROM 解压失败,改用
createFromFile(),把 db 放在/sdcard/Android/data/<pkg>/files/目录,随 APK 下发,首次启动校验 MD5 后再拷贝。 - 微信/QQ 清理缓存会删掉
/databases/外部文件,导致下次启动再次预填充;可在Application#onCreate中检测databaseExists()与文件 MD5,若被误删则走降级策略,提示用户“正在恢复数据”。 - 多进程同时触发预填充时,用
FileLock保证只有一个进程拷贝,其余进程阻塞等待,防止 SQLiteBusyException。
拓展思考
-
如何做到“增量预填充”?
把字典表拆成“只读基础包 + 可更新增量包”,基础包用createFromAsset,增量包用 WorkManager 后台下载,下载完成后在RoomDatabase.Callback#onOpen里执行ATTACH DATABASE 'incremental.db' AS inc; INSERT OR REPLACE INTO main.city SELECT * FROM inc.city; DETACH inc;,实现热更新。 -
预填充数据库的加密场景:
若整个 db 需要 SQLCipher 加密,官方createFromAsset不支持加密文件。解决思路:
- 预置明文 db → 首次启动拷贝 → 用 SQLCipher 重新加密导出 → 删除明文 → 后续 Room 使用
SupportFactory打开加密文件。 - 或者预置已加密的 db,但把密码拆成服务端动态下发,防止硬编码被逆向。
- Jetpack Compose + Room 的离线优先架构:
预填充数据作为“唯一可信源”,配合 Paging 3 RemoteMediator 实现“网络→DB→UI”单向数据流;当用户无网时直接读预置数据,有网时后台刷新,UI 层无感知,符合国内 5G 弱网场景体验要求。