上一篇我们聊了 KMM 的跨平台实践,这篇把视角拉回本地存储。SharedPreferences 陪伴 Android 开发者十几年了,但 Google 自己也在推 DataStore。到底要不要迁移?怎么迁?踩了什么坑?
为什么 SharedPreferences 不够用了
先说结论:SP 不是不能用,但在异步写入、类型安全和事务一致性上有先天缺陷。
SP 的几个老问题:
同步读取阻塞主线程。
getSharedPreferences().getString()在首次加载时会触发磁盘 IO,如果文件大,ANR 不是吓你的。apply()异步写盘但不可控。没有回调告诉你写完了,也没有错误处理。commit() 同步写盘但阻塞。不支持类型安全。所有 get/put 都是裸 key-value,key 拼错只有运行时才炸。
事务粒度太粗。多次 put 只在 commit/apply 时一次性写入,但中途如果崩溃,数据状态不确定。
DataStore 两种模式
Google 提供了两个实现:
大多数项目从 SP 迁移时,先用 Preferences DataStore 就够了。
基础用法
// 创建 DataStore
private val Context.dataStore by preferencesDataStore("settings")
// 定义 key
val KEY_DARK_MODE = booleanPreferencesKey("dark_mode")
val KEY_USER_ID = stringPreferencesKey("user_id")
// 写入
suspend fun saveSettings(darkMode: Boolean, userId: String) {
dataStore.edit { preferences ->
preferences[KEY_DARK_MODE] = darkMode
preferences[KEY_USER_ID] = userId
}
}
// 读取(Flow 响应式)
val darkMode: Flow<Boolean> = dataStore.data
.map { it[KEY_DARK_MODE] ?: false }
注意几个点:edit 是 suspend 函数,天然支持协程。读数据返回的是 Flow,可以跟 Compose 或 LiveData 无缝衔接。如果你只需要一次性读取,用 dataStore.data.first()。
从 SP 迁移
DataStore 内置了 SP 迁移工具:
private val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_prefs_name") { sp, _ ->
// 把 SP 的数据映射到 DataStore
val darkMode = sp.getBoolean("dark_mode", false)
mapOf(KEY_DARK_MODE to darkMode)
})
}
)
迁移发生在 DataStore 首次初始化时,执行完后 SP 文件不会被删除(你得自己删)。迁移是原子的——要么全部成功,要么回滚,不会出现半迁移状态。
Proto DataStore 的优势
当你的存储对象有嵌套结构时,Preferences DataStore 要手动管理多个 key,容易出错。Proto DataStore 直接序列化一个对象:
// user_prefs.proto
syntax = "proto3";
message UserPreferences {
bool dark_mode = 1;
string user_id = 2;
int32 font_size = 3;
}
class UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return UserPreferences.parseFrom(input)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
好处是类型安全、字段变更可追溯、序列化性能比 JSON 快不少。代价是要引入 protobuf 编译器。
实际迁移建议
如果你还在犹豫要不要迁,我的建议:
新项目直接上 DataStore,没有历史包袱就别用 SP 了。
存量项目按模块逐步迁移,不要一次性全换。先迁配置类数据,再迁用户偏好。
如果数据结构复杂且稳定,直接用 Proto DataStore,省得后面再迁一次。
SP 和 DataStore 可以共存,迁移期间两者并行,新代码用 DataStore,旧代码逐步替换。
一个踩坑记录
apply() 在 SP 里是异步的,但 DataStore 的 edit 是 suspend。如果你在 onPause() 里调 edit 而没有用 runBlocking,数据可能写不进去。解决方案是用 lifecycleScope.launch 确保协程有足够时间执行,或者用 DataStore 的 updateData 配合 runBlocking 做兜底。
小结
SharedPreferences 不是不能用,但 DataStore 在类型安全、响应式读取、错误处理上确实更好。迁移成本不高,建议新项目直接上,存量项目按模块逐步替换。
下一篇我们聊聊 Android 消息机制的核心:Handler/Looper/MessageQueue 到底怎么运作的。