尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Android随笔-DataStore

Android随笔-DataStore
📅 发布时间:2026/7/17 19:36:22

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]?:0

5. 写

// 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 操作,迁移只执行一次,靠目标文件是否存在来判断)。

六、常见坑与最佳实践

  1. 多实例冲突:同一文件两个 DataStore → IllegalStateException: DataStore cannot be used with multiple…。解决:一律用顶层 by preferencesDataStore 委托。
  2. 主线程 runBlocking { data.first() }:把异步 API 强行同步化,等于把 SP 的 ANR 问题手动搬回来。永远用 Flow 收集或协程挂起。
  3. 大对象/大数据:和 SP 一样是全量读写(改一个字段也是整个文件重写一遍)。超过几百 KB 的列表数据请用 Room。
  4. 高频写入:edit 每次都全量序列化 + fsync,埋点式高频写入要自己做批量/节流。
  5. IO 异常:文件读不出来(非 CorruptionException 的 IOException)会让 Flow 抛异常,UI 层记得 catch {}:
dataStore.data.catch{if(itisIOException)emit(emptyPreferences())elsethrowit}.map{...}
  1. 多进程:SingleProcessDataStore 明确只保证单进程。新版 datastore-core-multiprocess 提供了 MultiProcessDataStoreFactory(基于文件锁 + 版本协商),但成熟度不如单进程方案;重多进程场景还是看 MMKV。
  2. 版本兼容:读老文件时 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 的可变共享状态、锁竞争、主线程阻塞、写文件不原子四个历史顽疾。

相关新闻

  • 多策略同时发信号时,量化软件如何处理资金冲突
  • 小程序毕设项目:基于 SpringBoot + 微信小程序的猫咖探店预约小程序的设计与实现 (源码+文档,讲解、调试运行,定制等)
  • 企业级RAG知识库系统:Dify平台架构与实战指南

最新新闻

  • 2026不停车超限检测系统品牌排行榜,广州聚杰稳居行业前列 - 品牌速递
  • CM32M433R-START开发板:RISC-V生态与物联网应用实战
  • HarmonyOS掌上记账APP开发实践第28篇:自定义组件封装策略 — 从通用 Header 到业务组件的分层设计
  • AI Agent开发中的PLAN-AND-EXECUTE模式解析与实践
  • Ubuntu手动编译Linux内核全流程指南
  • 2026超限运输检测仪品牌排行榜,广州聚杰稳居行业前列 - 品牌速递

日新闻

  • 佛山青少年训练营推荐:军博营地实力顶尖 - 秋山寄远
  • 如何快速上手PvZ2 Gardendless:免费Web版植物大战僵尸2完整指南
  • jiuwen-deepsearch核心功能详解:规划-检索-反思三合一智能工作流

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号