18、数据持久化:Room数据库、SharedPreferences、媒体库元数据存储
各位同学,今天我们来聊聊车载应用里一个绕不开的话题——数据持久化。
说白了,就是怎么把用户的数据、配置、播放记录这些东西,稳稳当当地存下来。车机不像手机,它可能随时断电、重启,数据丢了可不是闹着玩的。我个人习惯把车载的数据持久化分成三个层次:轻量配置、结构化数据、媒体元数据。咱们一个一个来拆。
核心观点:选对存储方式,比会写代码更重要。SharedPreferences 存配置,Room 存业务数据,MediaStore 管媒体文件。混着用,会出大问题。
18.1 SharedPreferences:轻量级配置存储
先聊最简单的。SharedPreferences,本质上就是个 XML 文件,存一些 key-value 对。适合放什么?音量大小、上次选中的音源、夜间模式开关这类。
嗯,这里要注意:千万别往 SharedPreferences 里塞大对象或者列表。我在项目中遇到过有人把整个歌单 JSON 序列化后塞进去,结果读取时 ANR 了。你想想看,车机主线程读一个几百 KB 的 XML,不卡才怪。
我的习惯:用 SharedPreferences 只存基本类型和短字符串。如果需要存复杂结构,请用 Room。
// 保存配置
val prefs = context.getSharedPreferences("car_settings", Context.MODE_PRIVATE)
prefs.edit().putInt("last_volume", 15).apply()
// 读取配置
val volume = prefs.getInt("last_volume", 10) // 默认音量10
这里有个小坑:commit() 和 apply() 的区别。commit 是同步写入,会阻塞线程;apply 是异步写入。我建议在车载场景下,非关键配置一律用 apply。为什么?因为车机启动时,很多服务在抢资源,同步写文件可能会拖慢启动速度。
18.2 Room 数据库:结构化数据的正确打开方式
Room 是 Google 官方推荐的 ORM 框架,底层封装了 SQLite。说实话,在车载开发里,Room 是我用得最多的持久化方案。
为什么不用原生 SQLite?因为 Room 帮你做了三件事:编译时校验 SQL 语法、自动生成模板代码、与 LiveData/Flow 无缝集成。说白了,就是让你少写很多重复代码,而且不容易写错。
18.2.1 定义实体与 DAO
@Entity(tableName = "playlists")
data class Playlist(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val createTime: Long = System.currentTimeMillis(),
val songCount: Int = 0
)
@Dao
interface PlaylistDao {
@Query("SELECT * FROM playlists ORDER BY createTime DESC")
fun getAllPlaylists(): Flow<List<Playlist>>
@Insert
suspend fun insert(playlist: Playlist)
@Delete
suspend fun delete(playlist: Playlist)
}
看到 Flow 了吗?这是 Room 的亮点。数据一变,UI 自动更新。我在做车载音乐播放器时,歌单列表就是用 Flow 观察的。用户新建歌单后,列表立刻刷新,体验非常顺滑。
18.2.2 数据库升级:避坑指南
我曾经在项目上线后,发现需要给播放记录表加一个字段。当时图省事,直接改了实体类,没写 Migration。结果用户升级后,数据库直接崩溃了。
警告:生产环境中,绝对不要用 fallbackToDestructiveMigration()。这会导致用户数据全部丢失。一定要写 Migration 脚本。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE play_history ADD COLUMN play_duration INTEGER DEFAULT 0")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "car_media.db")
.addMigrations(MIGRATION_1_2)
.build()
18.3 媒体库元数据存储:MediaStore 的正确用法
车载多媒体应用,核心就是管理音乐、视频这些媒体文件。Android 系统提供了 MediaStore,用来统一管理媒体库的元数据。
你可能会问:为什么不用 Room 自己存?因为 MediaStore 是系统级的,其他应用(比如语音助手、系统音乐播放器)也能访问。而且系统媒体扫描服务会自动更新文件变化,省去你自己监听文件目录的麻烦。
18.3.1 查询媒体文件
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.DURATION
)
val cursor = contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
MediaStore.Audio.Media.TITLE + " ASC"
)
cursor?.use {
while (it.moveToNext()) {
val title = it.getString(it.getColumnIndex(MediaStore.Audio.Media.TITLE))
val artist = it.getString(it.getColumnIndex(MediaStore.Audio.Media.ARTIST))
// 处理歌曲信息
}
}
嗯,这里有个性能问题。如果媒体库里有几千首歌,一次性查询全部数据会占用大量内存。我建议分页查询,或者只查询当前需要展示的数据。
18.3.2 写入自定义元数据
有时候我们需要给媒体文件打上自己的标签,比如「我喜欢这首歌」。MediaStore 允许写入自定义字段,但要注意权限。
val values = ContentValues().apply {
put(MediaStore.Audio.Media.IS_FAVORITE, 1) // 标记为喜欢
}
contentResolver.update(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
values,
"${MediaStore.Audio.Media._ID} = ?",
arrayOf(songId.toString())
)
小技巧:如果只是应用内部使用的标记(比如「我的收藏」),我建议存在 Room 里,不要污染 MediaStore。MediaStore 的字段是全局共享的,其他应用也可能修改。
18.4 三种存储方式的选型对比
| 特性 | SharedPreferences | Room | MediaStore |
|---|---|---|---|
| 数据类型 | 基本类型 + String | 任意对象(通过实体映射) | 媒体文件元数据 |
| 查询能力 | 按 key 查询 | SQL 查询,支持复杂条件 | 按列查询,支持排序 |
| 线程安全 | 不保证 | 支持协程/Flow | 需要自行处理 |
| 数据共享 | 仅本应用 | 仅本应用 | 系统级共享 |
| 推荐场景 | 配置、偏好 | 业务数据、列表 | 音乐、视频标签 |
最后说一句:数据持久化没有银弹。选型时多想想你的数据会被谁用、多久用一次、丢了能不能接受。想清楚这些,代码怎么写都是对的。
总结:SharedPreferences 存配置,Room 存业务,MediaStore 管媒体。三者各司其职,别混着用。记住我在项目里踩过的坑,你就能少走弯路。