尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android随笔-DataStore VS SP
本文将从加载、读、写、线程模型、一致性、异常恢复六个维度逐个拆源码。SP 基于 AOSP 的 SharedPreferencesImplDataStore 基于 androidx.datastore 的 SingleProcessDataStore。一、整体架构对比SharedPreferencesDataStore核心类SharedPreferencesImplSingleProcessDataStore实现DataStore接口并发模型锁 内存 Map读写都在调用线程Actor 模型SimpleActorMessage所有读写请求串行排队读 API同步返回getString()FlowT冷流挂起函数写 APIEditor.commit()/apply()suspend fun updateData(transform: (T) - T)/edit {}存储格式XMLXmlUtils.writeMapXmlPreferences DataStore 用protobuf 二进制PreferencesSerializer最大的思想差异SP 是共享内存 异步落盘DataStore 是单写者 Actor 消息驱动状态机。二、加载流程SP构造即异步加载读时阻塞等待// SharedPreferencesImpl 构造函数SharedPreferencesImpl(Filefile,intmode){mFilefile;mBackupFilemakeBackupFile(file);mMapnull;startLoadFromDisk();// 构造时就开子线程加载}privatevoidstartLoadFromDisk(){synchronized(mLock){mLoadedfalse;}newThread(SharedPreferencesImpl-load){publicvoidrun(){loadFromDisk();}// 子线程解析 XML 全量进 mMap}.start();}读的时候如果还没加载完调用线程被锁死等publicStringgetString(Stringkey,NullableStringdefValue){synchronized(mLock){awaitLoadedLocked();// ← 主线程在这里干等 IO 完成Stringv(String)mMap.get(key);returnv!null?v:defValue;}}privatevoidawaitLoadedLocked(){while(!mLoaded){try{mLock.wait();}// Object.wait直接挂住调用线程catch(InterruptedExceptionunused){}}}这是同步 API 的原罪读 API 签名是同步的只能要么内存有、要么阻塞。DataStore懒加载 状态机DataStore 只有被第一次收集Flow或第一次 edit时才加载内部是一个明确的状态机// SingleProcessDataStore.ReadState 的几种状态UnInitialized// 未初始化fileExistenceKnown falseReadException// 读文件失败Data// 正常持有数据含一个 StateFlow 用于发射后续变更读文件通过协程挂起而不是阻塞线程privatesuspendfunreadAndInit(){// ... 拿到锁后valdataserializer.readFrom(input)// 挂起IO 线程执行不阻塞主线程updateData(data)// 状态机从 UnInitialized → Data}线程从始至终不等只是协程挂起恢复——这是二者读路径的本质区别。三、写流程核心差异所在SP内存先提交磁盘写塞进 QueuedWorkapply() 的完整链路// EditorImpl.apply()publicvoidapply(){finalMemoryCommitResultmcrcommitToMemory();// ① 内存同步改 mMap立即可见finalRunnableawaitCommit...;// 等磁盘写完的 RunnableQueuedWork.addFinisher(awaitCommit);// ② 注册一个 finisherANR 的伏笔...SharedPreferencesImpl.this.enqueueDiskWrite(mcr,postWriteRunnable);// ③ 磁盘任务入队}privatevoidenqueueDiskWrite(finalMemoryCommitResultmcr,finalRunnablepostWriteRunnable){finalRunnablewriteToDiskRunnable()-{synchronized(mWritingToDiskLock){writeToFile(mcr,isFromSyncCommit);// 真正序列化 XML、写文件}};QueuedWork.queue(writeToDiskRunnable,false/* 异步 */);}ANR 的根源在 QueuedWork系统在组件生命周期切换时强制清空队列// ActivityThread.handleStopService()publicvoidhandleStopService(IBindertoken){...QueuedWork.waitToFinish();// ← 主线程在这里同步等所有 SP 磁盘写完成}类似的调用点还有 performStopActivityInner()、handleServiceArgs()、handlePauseActivity()。所以 SP 写得多 → writeToFile 堆积 → Activity 跳转/Service 收发时主线程卡在 waitToFinish()。另外 SP 的磁盘写还有一个写放大问题writeToFileLocked 每次是把内存里整个 Map 全量序列化成 XML 重写整个文件// writeToFileLocked 内部strnewFileOutputStream(mFile);XmlUtils.writeMapXml(mapToWriteToDisk,str);// 全量重写改 1 个 key 也是整文件重写文件 100KB、每秒写 10 次就是每秒 1MB 的 IO。DataStoreActor 串行处理 UpdateMessageDataStore 的写是一条消息驱动的链路// SingleProcessDataStore.updateData()overridesuspendfunupdateData(transform:suspend(t:T)-T):T{valackCompletableDeferredT(coroutineContext[Job])valupdateMsgMessage.Update(transform,ack,...)actor.offer(updateMsg)// ① 把写请求作为消息投给 Actorreturnack.await()// ② 挂起等待这个写完成}SimpleActor 内部是一个 Channel 消费协程所有读写消息在一个协程里严格串行执行privatesuspendfunconsumeMessages(){for(msginmessageQueue){// Channel 循环消费天然串行msg.message.runTasks(...)// 按到达顺序依次执行读任务/写任务}}写文件时用writeScope先写临时文件再原子 rename// FileConnections.writeScopevalscratchFileFile(file.absolutePathSCRATCH_SUFFIX)// .tmp 临时文件writeTo(scratchFile){outputStream-serializer.writeTo(newData,outputStream)// protobuf 序列化写入}check(scratchFile.renameTo(file)){Unable to rename$scratchFile}// 原子替换维度SPDataStore写调度调用线程改内存 QueuedWork 异步线程落盘Actor 单协程串行处理所有消息写文件方式全量 XML 重写全量重写但走.tmprenameTo原子替换主线程风险点QueuedWork.waitToFinish()生命周期回调里无全部是挂起不占线程多次写的顺序靠mDiskWritesInFlight计数 版本号保证Channel 天然 FIFO 顺序四、线程模型对比SP 的线程足迹读调用线程可能 awaitLoadedLocked 阻塞 写内存调用线程 写磁盘QueuedWork 的单线程 HandlerThread异步 隐患waitToFinish() 在 handleStopActivity 等时机把磁盘 IO 拉回主线程DataStore 的线程足迹读Flow 收集所在协程文件 IO 在 readScopeDispatchers.IO 写updateData 挂起Actor 消费消息 文件 IO 都在 Dispatchers.IO 主线程只做 collect全程零阻塞一句话SP 是线程同步思维锁、wait/notifyDataStore 是协程同步思维挂起、Channel、Actor。前者在错误的线程调用就出事后者从机制上把 IO 摁死在后台调度器。五、一致性与事务SP单文件内一致跨进程不可靠单次 commit/apply 的多个 put 是一次 commitToMemory() 原子替换内存快照单进程内一致用 MemoryCommitResult.memoryStateGeneration 版本号保证apply 的顺序 落盘顺序但内存缓存是进程级的多进程下 A 进程写完B 进程的内存 Map 毫不知情。DataStoreupdateData 就是事务suspendfunupdateData(transform:suspend(T)-T):Ttransform 收到的是当前已落盘的最新快照它返回的新值会被原子写回。由于 Actor 串行执行消息两个并发 updateData 天然排队// 并发安全最终一定是 count 2SP 里同样的 read-modify-write 需要自己加锁dataStore.updateData{prefs-prefs.copy(countprefs.count1)}dataStore.updateData{prefs-prefs.copy(countprefs.count1)}读侧拿到的永远是 transform 完成后的快照不存在读到写一半的中间态。SingleProcessDataStore 从名字就声明了边界单进程内强一致多进程依然是禁区需要 MultiProcessDataStoreFactory 的方案。六、异常与数据损坏恢复SP几乎没有恢复手段XML 解析失败 → loadFromDisk 里 catch 后把 mMap 置为空 Map原文件数据全部丢失最多靠 .bak 备份文件只在上次写成功时存在。DataStoreCorruptionException 可插拔的恢复策略// 读文件抛 CorruptionException 时交给 handler 决定恢复值valdataStoreDataStoreFactory.create(serializerMySerializer,corruptionHandlerReplaceFileCorruptionHandler{DefaultPreferences}// 损坏时替换为默认值)readAndInit() 里捕获 CorruptionException 后调用 corruptionHandler.handleCorruption(ex) 拿到一个默认数据继续跑文件损坏不至于让 App 数据全丢/崩溃。七、总结SharedPreferencesImpl SingleProcessDataStore ───────────────────── ────────────────────── 构造 → 子线程 loadFromDisk 首次 collect/edit → readAndInit (XML 全量解析 → mMap) (protobuf 解析 → State Data) getString() → awaitLoadedLocked() data.first() → 挂起等 Actor 处理 ReadTask ↑ 主线程可能阻塞 ↑ 主线程只挂起不占线程 apply() updateData { } ├─ commitToMemory()同步改内存 → actor.offer(UpdateMessage) └─ QueuedWork.queue(写磁盘) → SimpleActor 串行消费 ↑ waitToFinish() 的 ANR 雷 → writeScope: .tmp → renameTo XML 全量重写 .bak protobuf 全量重写 原子 rename 损坏 → 数据清零 损坏 → CorruptionHandler 兜底 多进程 → 不可靠 多进程 → 明确不支持单进程语义八、迁移官方提供 SharedPreferencesMigration把 SP 数据一次性搬进 DataStorevalContext.dataStorebypreferencesDataStore(namesettings,produceMigrations{context-listOf(SharedPreferencesMigration(context,old_sp_name))})迁移只执行一次成功后 SP 文件里的 key 被清掉。注意迁移映射默认是全量迁移只迁部分 key 要自己传 keysToMigrate。九、源码级结论SP 的问题不是 API 老而是它的同步读 API决定了必须阻塞或全量内存常驻QueuedWork 的异步写决定了 ANR 风险内生于框架。DataStore 用协程挂起替代线程阻塞、用 Actor 串行替代锁竞争、用 protobuf 原子 rename 替代 XML bak是从根上重做的方案。
RELATED

相关推荐

做有水平的开发者需要的信息要素归纳

做有水平的开发者需要的信息要素归纳

做有水平的开发者需要的信息要素归纳 这里总结一下成为一个成熟的开发者 需要的信息要素 1.高质量,正常的需求 2.开发范式 1.高质量,正常的需求 我们拿到一个编程语言, 熟悉他的编程语法,开发环境,开发软件以后 要去了…

📅 2026/9/12 10:48:05
Garvan Institute of Medical Research加入Parse Biosciences的认证服务商网络

Garvan Institute of Medical Research加入Parse Biosciences的认证服务商网络

通过Garvan的Evercode WT服务,亚太地区的研究人员获得高质量单细胞测序的新选择 QIAGEN旗下公司、可扩展及普及型单细胞测序领域的领军企业Parse Biosciences今日宣布,Garvan Institute of Medical Research旗下的基因组学平台核心设施已加入其认证服务商…

📅 2026/7/21 11:52:56
韩妆走向全球:韩国创新重塑美妆增长,销售额飙升53%

韩妆走向全球:韩国创新重塑美妆增长,销售额飙升53%

NIQ最新数据显示,韩妆(K-Beauty)销售额同比增长53%,两年内增长131%。这凸显了区域美妆趋势、社交电商以及成分驱动的创新正在如何重塑全球美妆市场的增长格局。 消费者情报领域的全球领导者NIQ (NYSE: NIQ)今日发布了最新研究结果,显示韩妆已…

📅 2026/8/3 17:37:07
MORE NEWS

更多资讯

📰

论文降重避坑指南:如何识别不可靠的文本改写服务

引言:降重路上的那些“坑” 毕业论文写作进入冲刺阶段,降重和文本改写几乎是每位同学绕不开的一关。面对动辄 30% 以上的重复率要求,不少同学把希望寄托在各类降重服务上。然而,这类服务的质量参差不齐,稍不留神就可能…

📰

别让论文“降重”变成“降智”:如何甄别与规避不可靠的文本改写服务

引言:降重路上的“隐形陷阱” 毕业论文写作的最后一公里,往往伴随着查重率的焦虑。面对知网、维普等平台的严格检测,不少同学将希望寄托于各类“降重”“改写”服务。然而,这些服务良莠不齐,选错工具不仅浪费金钱&…

📰

Vant RollingText 翻滚文本组件实战指南:数字与文本翻滚动效的配置、原理与手动控制

Vant RollingText 翻滚文本组件实战指南:数字与文本翻滚动效的配置、原理与手动控制 【免费下载链接】vant A lightweight, customizable Vue UI library for mobile web apps. 项目地址: https://gitcode.com/GitHub_Trending/va/vant RollingText&#xff…

📰

ESP32-P4 USB Host U盘实验:TinyUSB与FATFS全链路解析

手里拿到正点原子DNESP32P4开发板,跑到《开发指南》第四十七章“USB U盘实验”时,我的第一反应是:这不就是把U盘插上、读个文件嘛。结果真正调起来才发现,这个实验比想象中复杂得多。单片机要完成USB设备枚举、Mass Storage协议交…

📰

帝国CMS 7.5后台响应式模板V2.4:安装部署与二次开发指南

简介:帝国CMS7.5响应式后台管理系统界面模板美化版V2.4是一套专为帝国CMS 7.5开发者与管理后台日常使用场景设计的美化方案,同时提供GBK与UTF8双编码,基于响应式布局自适应电脑、手机等多分辨率终端,解决原始后台界面老旧、在部分…

📰

动态规划解决含手续费股票交易问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬