DataStore是什么
DataStore 是 Jetpack 推出的异步、事务性、类型安全的数据存储库,定位是彻底替代 SharedPreferences。它建立在 Kotlin 协程 + Flow 之上,分两种形态:
| 形态 | 存储方式 | 类型安全 | 对标 |
|---|---|---|---|
| Preferences DataStore | 键值对(protobuf 二进制) | 弱(和 SP 一样靠 key) | SP 的直接替代 |
| Proto DataStore | 自定义 protobuf schema | 强(编译期生成的类型) | 结构化配置/小型数据模型 |
文件存储在 /data/data/<包名>/files/datastore/<文件名>(不是 SP 的 shared_prefs 目录)。
二、Preferences DataStore 使用
1. 依赖
groovy
implementation "androidx.datastore:datastore-preferences:1.1.1"
2. 定义(必须是进程内单例)
kotlin
// 写在文件顶层,by 委托保证全局唯一实例
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
必须单例:同一个文件创建多个 DataStore 实例会直接抛 IllegalStateException(文件锁冲突),这是最常见的坑。by preferencesDataStore 委托内部会缓存,天然解决。
3. 定义 key
kotlin
object PrefKeys {
val LAUNCH_COUNT = intPreferencesKey("launch_count")
val USER_NAME = stringPreferencesKey("user_name")
val DARK_MODE = booleanPreferencesKey("dark_mode")
val LAST_TS = longPreferencesKey("last_timestamp")
// 还有 float / double / stringSet / byteArrayPreferencesKey
}
4. 读
kotlin
// data 是 Flow<Preferences>,每次变更都会发射新的完整快照
val nameFlow: Flow<String> = context.settingsDataStore.data
.map { prefs -> prefs[PrefKeys.USER_NAME] ?: "" }
// 在 ViewModel 里转成 StateFlow
val name = nameFlow.stateIn(viewModelScope, SharingStarted.Eagerly, "")
// 只想取一次(注意是挂起函数)
val count = context.settingsDataStore.data.first()[PrefKeys.LAUNCH_COUNT] ?: 0
5. 写
kotlin
// Preferences DataStore 的 edit 等价于 Proto DataStore 的 updateData
context.settingsDataStore.edit { prefs ->
prefs[PrefKeys.LAUNCH_COUNT] = (prefs[PrefKeys.LAUNCH_COUNT] ?: 0) + 1
prefs.remove(PrefKeys.USER_NAME) // 删除
// prefs.clear() // 清空
}
edit{} 是一个事务块:lambda 里拿到的 MutablePreferences 是当前已落盘数据的快照,lambda 返回后整体原子写回。多个并发 edit 会严格排队(见原理部分)。
三、内部原理(源码级)
核心实现类是 SingleProcessDataStore,理解了它就理解了 DataStore 的 90%。
1. 状态机:ReadState
DataStore 内部的数据持有是一个显式状态机:
kotlin
sealed class ReadState<T> {
class UnInitialized // 还没读过文件
class ReadException // 读文件炸了(不可恢复异常)
class Data<T>( // 正常状态
val value: T, // 当前数据快照(不可变对象)
val hashCode: Int,
val updateLock: ...
) : ReadState<T>()
class Final<T> // scope 取消,终态
}
注意 Data 里持有的 value 是不可变对象 (ImmutablePreferences)。每次 edit 不是原地改,而是生成一个全新的快照替换掉旧引用------这就是 Flow 的订阅者能拿到"新数据"的原因,也是写时复制(copy-on-write)思路。
2. 并发核心:SimpleActor(单写者模型)
这是 DataStore 最精华的设计。所有读写请求都被包装成 Message,投递给一个 Actor 严格串行处理:
kotlin
// SingleProcessDataStore
private val actor = SimpleActor<Message<T>>(
scope = scope,
onComplete = { ... },
consumeMessage = { message ->
when (message) {
is Message.Read -> handleRead(message)
is Message.Update -> handleUpdate(message)
}
}
)
SimpleActor 内部结构:
kotlin
class SimpleActor<T>(scope, ..., consumeMessage) {
private val messageQueue = Channel<T>(capacity = UNLIMITED) // 无界 Channel 当消息队列
init {
scope.launch { consumeMessages() }
}
private suspend fun consumeMessages() {
for (msg in messageQueue) { // for 循环收 Channel,FIFO 严格串行
consumeMessage(msg)
}
}
fun offer(msg: T) { messageQueue.trySend(msg) }
}
这意味着:不存在锁竞争,不存在并发写文件,天然线程安全。你开 100 个协程同时 edit,它们只是在往 Channel 里投消息,落盘永远一个接一个。
3. 读流程:从收集 Flow 到发射数据
kotlin
override val data: Flow<T> = flow {
// ① 先发当前内存快照(如果已初始化)
val currentState = readState.value
if (currentState is Data) emit(currentState.value)
// ② 订阅后续变更
downstreamFlow.collect { emit(it) }
}.onStart {
actor.offer(Message.Read(...)) // ③ 有人收集了,触发初始化读盘
}
当状态是 UnInitialized 时,第一条 Read 消息触发 readAndInit():
kotlin
private suspend fun readAndInit() {
// 双重检查,只初始化一次
val initData = FileInputStream(file).use { serializer.readFrom(it) } // 挂起,在 IO 调度器
updateData(initData) // 状态机 UnInitialized → Data
// 然后依次补跑排队的 Update 任务
runTasks()
}
要点:读文件是挂起操作 ,执行在 Dispatchers.IO,主线程只是等协程恢复,线程本身没被占。
4. 写流程:updateData 的完整链路
kotlin
override suspend fun updateData(transform: suspend (t: T) -> T): T {
val ack = CompletableDeferred<T>(coroutineContext[Job])
val updateMsg = Message.Update(transform, ack, currentState, callerContext)
actor.offer(updateMsg) // 投消息
return ack.await() // 挂起等结果
}
Actor 消费到 Update 消息时:
kotlin
// handleUpdate → transformAndWrite
val newData = update.transform(curValue) // ① 执行你的 lambda,拿到新数据
writeScope { outputStream ->
serializer.writeTo(newData, outputStream) // ② 序列化写文件
}
ack.complete(newData) // ③ 唤醒挂起的调用方
事务性的来源:transform 拿到的 curValue 一定是"上一个 Update 落盘后的最新值",且 Actor 串行保证两个 Update 不会交错。read-modify-write 天然正确,不需要任何锁。
5. 落盘:临时文件 + 原子 rename
kotlin
// FileConnections.writeScope
val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX) // "<name>.tmp"
scratchFile.createNewFile()
FileOutputStream(scratchFile).use { stream ->
serializer.writeTo(newData, stream)
stream.fd.sync() // fsync 刷盘
}
if (!scratchFile.renameTo(file)) throw IOException("Unable to rename")
三个保证:
- fsync:数据真正进磁盘,不是 page cache;
- rename 原子性:写一半崩溃,要么旧文件完整、要么新文件完整,不存在半截文件(对比 SP 的 .bak 备份方案,这是更干净的解法);
- .tmp 残留由启动时 cleanUp 处理。
6. 损坏恢复:CorruptionHandler
文件损坏(解密失败、半截 protobuf)时 serializer.readFrom 抛 CorruptionException:
kotlin
val Context.safeStore by preferencesDataStore(
name = "settings",
corruptionHandler = ReplaceFileCorruptionHandler { ex ->
emptyPreferences() // 损坏时用空配置重启,而不是崩溃
}
)
内部 readAndInit 捕获后调用 handler 拿到替代值继续初始化,状态机进入 Data 而不是 ReadException。
7. 作用域与生命周期
DataStoreFactory.create(scope = ...) 里的 scope 决定:
- IO 跑在哪个调度器(默认 Dispatchers.IO);
- Actor 消费协程的生命周期------scope 取消,DataStore 进入 Final 态,再访问抛异常。
用 by preferencesDataStore 委托时默认 scope 是应用级的,正常不用管。
四、Proto DataStore
当需要强类型 + 自定义结构时使用。三步:
1. 定义 schema(.proto 文件)
proto
syntax = "proto3";
option java_package = "com.example.proto";
option java_multiple_files = true;
message UserSettings {
string user_name = 1;
int32 launch_count = 2;
bool dark_mode = 3;
}
protobuf 插件编译后生成 Java/Kotlin 类。
2. 实现 Serializer
kotlin
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings =
try {
UserSettings.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", e)
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) =
t.writeTo(output)
}
3. 创建与使用
kotlin
val Context.userStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
// 写:builder 模式
context.userStore.updateData { current ->
current.toBuilder()
.setLaunchCount(current.launchCount + 1)
.build()
}
// 读
val settings = context.userStore.data.first()
Preferences vs Proto 怎么选:散落的开关/标记用 Preferences(省去维护 proto);有明确领域模型、字段间有关联的用 Proto(编译期类型检查 + 默认值管理)。
五、从 SP 迁移
kotlin
val Context.settingsDataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context = context,
sharedPreferencesName = "legacy_sp"
// keysToMigrate = setOf("a", "b") // 可选:只迁指定 key
) { prefs, current -> // 可选的映射 lambda,自定义转换逻辑
current.toMutablePreferences().apply {
putAll(prefs.all.map { (k, v) -> ... })
}.toPreferences()
}
)
}
)
机制:首次 readAndInit 时如果 datastore 文件不存在,按顺序跑所有 Migration,把 SP 数据转换后作为初始值落盘,然后从 SP 删掉已迁移的 key(用 SharedPreferencesView 操作,迁移只执行一次,靠目标文件是否存在来判断)。
六、常见坑与最佳实践
- 多实例冲突:同一文件两个 DataStore → IllegalStateException: DataStore cannot be used with multiple...。解决:一律用顶层 by preferencesDataStore 委托。
- 主线程 runBlocking { data.first() }:把异步 API 强行同步化,等于把 SP 的 ANR 问题手动搬回来。永远用 Flow 收集或协程挂起。
- 大对象/大数据:和 SP 一样是全量读写(改一个字段也是整个文件重写一遍)。超过几百 KB 的列表数据请用 Room。
- 高频写入:edit 每次都全量序列化 + fsync,埋点式高频写入要自己做批量/节流。
- IO 异常:文件读不出来(非 CorruptionException 的 IOException)会让 Flow 抛异常,UI 层记得 catch {}:
kotlin
dataStore.data
.catch { if (it is IOException) emit(emptyPreferences()) else throw it }
.map { ... }
- 多进程:SingleProcessDataStore 明确只保证单进程。新版 datastore-core-multiprocess 提供了 MultiProcessDataStoreFactory(基于文件锁 + 版本协商),但成熟度不如单进程方案;重多进程场景还是看 MMKV。
- 版本兼容:读老文件时 protobuf 字段号是兼容的关键------字段只能新增、不能改号、不能删号(删了要 reserved)。
七、总结
你的代码 SingleProcessDataStore 内部
───────── ──────────────────────────────
data.collect {} ──offer──▶ Message.Read ─┐
│
edit { ... } ──offer──▶ Message.Update│ SimpleActor (Channel + 单协程)
updateData { } ──offer──▶ Message.Update┴──▶ 严格 FIFO 串行消费
│
ReadState 状态机 ◀─────┘
UnInitialized → Data → Final
│
读: serializer.readFrom (Dispatchers.IO)
写: .tmp 文件 + fsync + renameTo 原子替换
│
变更 → StateFlow 发射新 Immutable 快照 → 下游 Flow
一句话总结 :DataStore = 不可变快照 + Actor 单写者串行化 + 协程挂起 IO + 原子文件替换,四个设计分别解决了 SP 的可变共享状态、锁竞争、主线程阻塞、写文件不原子四个历史顽疾。