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是从根上重做的方案。

相关新闻

Spring AI配置API Key的5种方式:从本地开发到Kubernetes生产环境

Spring AI配置API Key的5种方式:从本地开发到Kubernetes生产环境

前言 很多教程直接把API Key写在 application.yml 中: spring:ai:deepseek:api-key: sk-xxxxxxxx这种写法虽然能运行,却不适合真实项目。一旦代码被提交到Git、上传到CSDN、打包进镜像或发送给同事,密钥就可能永久泄露。 API Key泄露可能造成: 账户余额被盗刷; 企业数据…

2026/10/17 21:01:39 阅读更多 →
S19.3海外冷启动——Product Hunt、Reddit、Twitter/X的玩法

S19.3海外冷启动——Product Hunt、Reddit、Twitter/X的玩法

AI产品出海实战 第3篇:海外冷启动——Product Hunt、Reddit、Twitter/X的玩法导读:在国内,你可以在微信群、朋友圈、知乎、B站推广产品。但出海后,这些渠道都不存在了。你面对的是一个全新的社交媒体生态。Product Hunt是什么&…

2026/10/17 14:25:08 阅读更多 →
S18.1 AI产品冷启动——没有预算怎么获取前1000个用户

S18.1 AI产品冷启动——没有预算怎么获取前1000个用户

AI产品冷启动——没有预算怎么获取前1000个用户 本专栏面向AI产品创业者、增长负责人和独立开发者,系统拆解从0到10万用户的全链路增长方法论。这是第1篇,聚焦冷启动阶段。如果你正在为"产品上线了但没人用"而焦虑,这篇文章就是为你…

2026/10/17 14:28:23 阅读更多 →

最新新闻

约瑟夫环C语言循环链表实现:从建环到出圈的完整方案

约瑟夫环C语言循环链表实现:从建环到出圈的完整方案

简介:这是一份面向C语言初学者与数据结构学习者的约瑟夫死亡游戏完整实现资料,围绕循环链表这一核心结构展开,帮助读者理解环形链表的创建、遍历与结点删除等经典操作。资源包内仅含1个docx文档,大小约18KB,以文字形式…

2026/10/17 21:01:07 阅读更多 →
数据库连接报无监听程序?从监听器到防火墙的排查指南

数据库连接报无监听程序?从监听器到防火墙的排查指南

数据库连接报“无监听程序”,应该是我这些年处理过出现频率最高的问题之一。它的迷惑性在于:报错文案就那么一句话,看起来就是“连不上”,但背后的原因可以从监听服务没开、配置文件丢失,一路延伸到主机名解析和防火墙…

2026/10/17 21:01:07 阅读更多 →
乐企数字开放平台测试中心沙箱联调实操指南:从能力订阅到报告提交

乐企数字开放平台测试中心沙箱联调实操指南:从能力订阅到报告提交

简介:《乐企数字开放平台操作手册-测试中心分册V2.000》面向企业纳税人及平台对接技术人员,聚焦测试中心沙箱测试与能力测试的完整操作流程,帮助读者在模拟环境中评估和验证平台各项能力。手册围绕概述、能力中心、测试中心三部分展开&#x…

2026/10/17 21:01:07 阅读更多 →
socket错误处理封装实战:从errno分类到统一重试机制

socket错误处理封装实战:从errno分类到统一重试机制

1. 从一次线上连接异常说起:socket 错误处理的真实痛点做网络编程的人大概都有过这样的经历:本地跑得好好的服务,一上线就时不时报出一些莫名其妙的错误码,日志里打印出来的信息要么是-1,要么是一串看不懂的 errno 数字…

2026/10/17 21:01:07 阅读更多 →
数据中心网络传输新思路:接收端驱动与包级交织的实践解读

数据中心网络传输新思路:接收端驱动与包级交织的实践解读

做数据中心网络的同行应该都清楚,“数据包传输”这个词听起来基础,真正把它做到极致却极其折磨人。最近我把 NSDI 2021 那篇《A New Approach to Datacenter Packet Transport》的论文又完整啃了一遍,结合我们自己在实验环境里折腾拥塞控制、…

2026/10/17 21:01:07 阅读更多 →
Kepware OPC DA Client配置实战:三层结构与QC质量码诊断

Kepware OPC DA Client配置实战:三层结构与QC质量码诊断

简介:本资源是一份面向工业物联网(IIoT)开发者与自动化工程师的Kepware OPC DA Client实战入门教程,聚焦于使用KEPServerEX构建稳定、高效的数据采集通道。内容从零开始详解项目(Project)创建、Channel配置…

2026/10/17 21:00:06 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →