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. 依赖
implementation"androidx.datastore:datastore-preferences:1.1.1"2. 定义(必须是进程内单例)
// 写在文件顶层,by 委托保证全局唯一实例valContext.settingsDataStore:DataStore<Preferences>bypreferencesDataStore(name="settings")必须单例:同一个文件创建多个 DataStore 实例会直接抛 IllegalStateException(文件锁冲突),这是最常见的坑。by preferencesDataStore 委托内部会缓存,天然解决。
3. 定义 key
objectPrefKeys{valLAUNCH_COUNT=intPreferencesKey("launch_count")valUSER_NAME=stringPreferencesKey("user_name")valDARK_MODE=booleanPreferencesKey("dark_mode")valLAST_TS=longPreferencesKey("last_timestamp")// 还有 float / double / stringSet / byteArrayPreferencesKey}4. 读
// data 是 Flow<Preferences>,每次变更都会发射新的完整快照valnameFlow:Flow<String>=context.settingsDataStore.data.map{prefs->prefs[PrefKeys.USER_NAME]?:""}// 在 ViewModel 里转成 StateFlowvalname=nameFlow.stateIn(viewModelScope,SharingStarted.Eagerly,"")// 只想取一次(注意是挂起函数)valcount=context.settingsDataStore.data.first()[PrefKeys.LAUNCH_COUNT]?:05. 写
// Preferences DataStore 的 edit 等价于 Proto DataStore 的 updateDatacontext.settingsDataStore.edit{prefs->prefs[PrefKeys.LAUNCH_COUNT]=(prefs[PrefKeys.LAUNCH_COUNT]?:0)+1prefs.remove(PrefKeys.USER_NAME)// 删除// prefs.clear() // 清空}edit{} 是一个事务块:lambda 里拿到的 MutablePreferences 是当前已落盘数据的快照,lambda 返回后整体原子写回。多个并发 edit 会严格排队(见原理部分)。
三、内部原理(源码级)
核心实现类是SingleProcessDataStore,理解了它就理解了 DataStore 的 90%。
1. 状态机:ReadState
DataStore 内部的数据持有是一个显式状态机:
sealedclassReadState<T>{classUnInitialized// 还没读过文件classReadException// 读文件炸了(不可恢复异常)classData<T>(// 正常状态valvalue:T,// 当前数据快照(不可变对象)valhashCode:Int,valupdateLock:...):ReadState<T>()classFinal<T>// scope 取消,终态}注意 Data 里持有的 value 是不可变对象(ImmutablePreferences)。每次 edit 不是原地改,而是生成一个全新的快照替换掉旧引用——这就是 Flow 的订阅者能拿到"新数据"的原因,也是写时复制(copy-on-write)思路。
2. 并发核心:SimpleActor(单写者模型)
这是 DataStore 最精华的设计。所有读写请求都被包装成 Message,投递给一个 Actor 严格串行处理:
// SingleProcessDataStoreprivatevalactor=SimpleActor<Message<T>>(scope=scope,onComplete={...},consumeMessage={message->when(message){isMessage.Read->handleRead(message)isMessage.Update->handleUpdate(message)}})SimpleActor 内部结构:
classSimpleActor<T>(scope,...,consumeMessage){privatevalmessageQueue=Channel<T>(capacity=UNLIMITED)// 无界 Channel 当消息队列init{scope.launch{consumeMessages()}}privatesuspendfunconsumeMessages(){for(msginmessageQueue){// for 循环收 Channel,FIFO 严格串行consumeMessage(msg)}}funoffer(msg:T){messageQueue.trySend(msg)}}这意味着:不存在锁竞争,不存在并发写文件,天然线程安全。你开 100 个协程同时 edit,它们只是在往 Channel 里投消息,落盘永远一个接一个。
3. 读流程:从收集 Flow 到发射数据
overridevaldata:Flow<T>=flow{// ① 先发当前内存快照(如果已初始化)valcurrentState=readState.valueif(currentStateisData)emit(currentState.value)// ② 订阅后续变更downstreamFlow.collect{emit(it)}}.onStart{actor.offer(Message.Read(...))// ③ 有人收集了,触发初始化读盘}当状态是 UnInitialized 时,第一条 Read 消息触发 readAndInit():
privatesuspendfunreadAndInit(){// 双重检查,只初始化一次valinitData=FileInputStream(file).use{serializer.readFrom(it)}// 挂起,在 IO 调度器updateData(initData)// 状态机 UnInitialized → Data// 然后依次补跑排队的 Update 任务runTasks()}要点:读文件是挂起操作,执行在 Dispatchers.IO,主线程只是等协程恢复,线程本身没被占。
4. 写流程:updateData 的完整链路
overridesuspendfunupdateData(transform:suspend(t:T)->T):T{valack=CompletableDeferred<T>(coroutineContext[Job])valupdateMsg=Message.Update(transform,ack,currentState,callerContext)actor.offer(updateMsg)// 投消息returnack.await()// 挂起等结果}Actor 消费到 Update 消息时:
// handleUpdate → transformAndWritevalnewData=update.transform(curValue)// ① 执行你的 lambda,拿到新数据writeScope{outputStream->serializer.writeTo(newData,outputStream)// ② 序列化写文件}ack.complete(newData)// ③ 唤醒挂起的调用方事务性的来源:transform 拿到的 curValue 一定是"上一个 Update 落盘后的最新值",且 Actor 串行保证两个 Update 不会交错。read-modify-write 天然正确,不需要任何锁。
5. 落盘:临时文件 + 原子 rename
// FileConnections.writeScopevalscratchFile=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))throwIOException("Unable to rename")三个保证:
- fsync:数据真正进磁盘,不是 page cache;
- rename原子性:写一半崩溃,要么旧文件完整、要么新文件完整,不存在半截文件(对比 SP 的 .bak 备份方案,这是更干净的解法);
- .tmp 残留由启动时 cleanUp 处理。
6. 损坏恢复:CorruptionHandler
文件损坏(解密失败、半截 protobuf)时 serializer.readFrom 抛 CorruptionException:
valContext.safeStorebypreferencesDataStore(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 文件)
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
objectUserSettingsSerializer:Serializer<UserSettings>{overridevaldefaultValue:UserSettings=UserSettings.getDefaultInstance()overridesuspendfunreadFrom(input:InputStream):UserSettings=try{UserSettings.parseFrom(input)}catch(e:InvalidProtocolBufferException){throwCorruptionException("Cannot read proto.",e)}overridesuspendfunwriteTo(t:UserSettings,output:OutputStream)=t.writeTo(output)}3. 创建与使用
valContext.userStore:DataStore<UserSettings>bydataStore(fileName="user_settings.pb",serializer=UserSettingsSerializer)// 写:builder 模式context.userStore.updateData{current->current.toBuilder().setLaunchCount(current.launchCount+1).build()}// 读valsettings=context.userStore.data.first()Preferences vs Proto怎么选:散落的开关/标记用 Preferences(省去维护 proto);有明确领域模型、字段间有关联的用 Proto(编译期类型检查 + 默认值管理)。
五、从 SP 迁移
valContext.settingsDataStorebypreferencesDataStore(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 {}:
dataStore.data.catch{if(itisIOException)emit(emptyPreferences())elsethrowit}.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 的可变共享状态、锁竞争、主线程阻塞、写文件不原子四个历史顽疾。