Android随笔-DataStore DataStore是什么DataStore 是 Jetpack 推出的异步、事务性、类型安全的数据存储库定位是彻底替代 SharedPreferences。它建立在 Kotlin 协程 Flow 之上分两种形态形态存储方式类型安全对标Preferences DataStore键值对protobuf 二进制弱和 SP 一样靠 keySP 的直接替代Proto DataStore自定义 protobuf schema强编译期生成的类型结构化配置/小型数据模型文件存储在 /data/data/包名/files/datastore/文件名不是 SP 的 shared_prefs 目录。二、Preferences DataStore 使用1. 依赖implementationandroidx.datastore:datastore-preferences:1.1.12. 定义必须是进程内单例// 写在文件顶层by 委托保证全局唯一实例valContext.settingsDataStore:DataStorePreferencesbypreferencesDataStore(namesettings)必须单例同一个文件创建多个 DataStore 实例会直接抛 IllegalStateException文件锁冲突这是最常见的坑。by preferencesDataStore 委托内部会缓存天然解决。3. 定义 keyobjectPrefKeys{valLAUNCH_COUNTintPreferencesKey(launch_count)valUSER_NAMEstringPreferencesKey(user_name)valDARK_MODEbooleanPreferencesKey(dark_mode)valLAST_TSlongPreferencesKey(last_timestamp)// 还有 float / double / stringSet / byteArrayPreferencesKey}4. 读// data 是 FlowPreferences每次变更都会发射新的完整快照valnameFlow:FlowStringcontext.settingsDataStore.data.map{prefs-prefs[PrefKeys.USER_NAME]?:}// 在 ViewModel 里转成 StateFlowvalnamenameFlow.stateIn(viewModelScope,SharingStarted.Eagerly,)// 只想取一次注意是挂起函数valcountcontext.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. 状态机ReadStateDataStore 内部的数据持有是一个显式状态机sealedclassReadStateT{classUnInitialized// 还没读过文件classReadException// 读文件炸了不可恢复异常classDataT(// 正常状态valvalue:T,// 当前数据快照不可变对象valhashCode:Int,valupdateLock:...):ReadStateT()classFinalT// scope 取消终态}注意 Data 里持有的 value 是不可变对象ImmutablePreferences。每次 edit 不是原地改而是生成一个全新的快照替换掉旧引用——这就是 Flow 的订阅者能拿到新数据的原因也是写时复制copy-on-write思路。2. 并发核心SimpleActor单写者模型这是 DataStore 最精华的设计。所有读写请求都被包装成 Message投递给一个 Actor 严格串行处理// SingleProcessDataStoreprivatevalactorSimpleActorMessageT(scopescope,onComplete{...},consumeMessage{message-when(message){isMessage.Read-handleRead(message)isMessage.Update-handleUpdate(message)}})SimpleActor 内部结构classSimpleActorT(scope,...,consumeMessage){privatevalmessageQueueChannelT(capacityUNLIMITED)// 无界 Channel 当消息队列init{scope.launch{consumeMessages()}}privatesuspendfunconsumeMessages(){for(msginmessageQueue){// for 循环收 ChannelFIFO 严格串行consumeMessage(msg)}}funoffer(msg:T){messageQueue.trySend(msg)}}这意味着不存在锁竞争不存在并发写文件天然线程安全。你开 100 个协程同时 edit它们只是在往 Channel 里投消息落盘永远一个接一个。3. 读流程从收集 Flow 到发射数据overridevaldata:FlowTflow{// ① 先发当前内存快照如果已初始化valcurrentStatereadState.valueif(currentStateisData)emit(currentState.value)// ② 订阅后续变更downstreamFlow.collect{emit(it)}}.onStart{actor.offer(Message.Read(...))// ③ 有人收集了触发初始化读盘}当状态是 UnInitialized 时第一条 Read 消息触发 readAndInit()privatesuspendfunreadAndInit(){// 双重检查只初始化一次valinitDataFileInputStream(file).use{serializer.readFrom(it)}// 挂起在 IO 调度器updateData(initData)// 状态机 UnInitialized → Data// 然后依次补跑排队的 Update 任务runTasks()}要点读文件是挂起操作执行在 Dispatchers.IO主线程只是等协程恢复线程本身没被占。4. 写流程updateData 的完整链路overridesuspendfunupdateData(transform:suspend(t:T)-T):T{valackCompletableDeferredT(coroutineContext[Job])valupdateMsgMessage.Update(transform,ack,currentState,callerContext)actor.offer(updateMsg)// 投消息returnack.await()// 挂起等结果}Actor 消费到 Update 消息时// handleUpdate → transformAndWritevalnewDataupdate.transform(curValue)// ① 执行你的 lambda拿到新数据writeScope{outputStream-serializer.writeTo(newData,outputStream)// ② 序列化写文件}ack.complete(newData)// ③ 唤醒挂起的调用方事务性的来源transform 拿到的 curValue 一定是上一个 Update 落盘后的最新值且 Actor 串行保证两个 Update 不会交错。read-modify-write 天然正确不需要任何锁。5. 落盘临时文件 原子 rename// FileConnections.writeScopevalscratchFileFile(file.absolutePathSCRATCH_SUFFIX)// name.tmpscratchFile.createNewFile()FileOutputStream(scratchFile).use{stream-serializer.writeTo(newData,stream)stream.fd.sync()// fsync 刷盘}if(!scratchFile.renameTo(file))throwIOException(Unable to rename)三个保证fsync数据真正进磁盘不是 page cacherename原子性写一半崩溃要么旧文件完整、要么新文件完整不存在半截文件对比 SP 的 .bak 备份方案这是更干净的解法.tmp 残留由启动时 cleanUp 处理。6. 损坏恢复CorruptionHandler文件损坏解密失败、半截 protobuf时 serializer.readFrom 抛 CorruptionExceptionvalContext.safeStorebypreferencesDataStore(namesettings,corruptionHandlerReplaceFileCorruptionHandler{ex-emptyPreferences()// 损坏时用空配置重启而不是崩溃})内部 readAndInit 捕获后调用 handler 拿到替代值继续初始化状态机进入 Data 而不是 ReadException。7. 作用域与生命周期DataStoreFactory.create(scope …) 里的 scope 决定IO 跑在哪个调度器默认 Dispatchers.IOActor 消费协程的生命周期——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. 实现 SerializerobjectUserSettingsSerializer:SerializerUserSettings{overridevaldefaultValue:UserSettingsUserSettings.getDefaultInstance()overridesuspendfunreadFrom(input:InputStream):UserSettingstry{UserSettings.parseFrom(input)}catch(e:InvalidProtocolBufferException){throwCorruptionException(Cannot read proto.,e)}overridesuspendfunwriteTo(t:UserSettings,output:OutputStream)t.writeTo(output)}3. 创建与使用valContext.userStore:DataStoreUserSettingsbydataStore(fileNameuser_settings.pb,serializerUserSettingsSerializer)// 写builder 模式context.userStore.updateData{current-current.toBuilder().setLaunchCount(current.launchCount1).build()}// 读valsettingscontext.userStore.data.first()Preferences vs Proto怎么选散落的开关/标记用 Preferences省去维护 proto有明确领域模型、字段间有关联的用 Proto编译期类型检查 默认值管理。五、从 SP 迁移valContext.settingsDataStorebypreferencesDataStore(namesettings,produceMigrations{context-listOf(SharedPreferencesMigration(contextcontext,sharedPreferencesNamelegacy_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 的可变共享状态、锁竞争、主线程阻塞、写文件不原子四个历史顽疾。