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/7/24 6:45:56 阅读更多 →
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/7/24 7:34:45 阅读更多 →
S18.1 AI产品冷启动——没有预算怎么获取前1000个用户

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

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

2026/7/24 4:55:50 阅读更多 →

最新新闻

华为OD机试真题 新系统 2026-07-19 PythonJS 实现【酒店服务记录分析】

华为OD机试真题 新系统 2026-07-19 PythonJS 实现【酒店服务记录分析】

目录 题目 思路 Code 题目 题目内容: 你是某连锁酒店的数据分析师。 酒店每天都会用一串编码记录各分店收到的服务请求类型。 编码由小写字母 a 到 z 组成,每个字母代表一种特定类型的服务请求。 为了优化资源分配,总部需要找出当天…

2026/7/24 17:16:13 阅读更多 →
ADS8353/ADS7853评估套件实战:从硬件连接到性能分析的完整指南

ADS8353/ADS7853评估套件实战:从硬件连接到性能分析的完整指南

1. 项目概述:深入解析ADS8353/ADS7853评估套件在高速数据采集、工业自动化或者精密仪器仪表的设计中,选对一颗模数转换器(ADC)往往是项目成败的关键。但数据手册上的参数再漂亮,终究是纸上谈兵。如何在实际的电路板、真…

2026/7/24 17:16:13 阅读更多 →
LoRA微调技术:大模型高效适配的实践指南

LoRA微调技术:大模型高效适配的实践指南

1. LoRA微调技术概述在自然语言处理领域,大模型微调一直是个让人又爱又恨的技术。传统全参数微调需要消耗大量计算资源,动辄需要几十GB显存,让很多研究者和开发者望而却步。而LoRA(Low-Rank Adaptation)技术的出现&…

2026/7/24 17:16:13 阅读更多 →
用Deno 2.0构建现代化后端:比Node.js更安全、更简洁

用Deno 2.0构建现代化后端:比Node.js更安全、更简洁

用Deno 2.0构建现代化后端:比Node.js更安全、更简洁 Node.js的"历史包袱"问题 Node.js已经18岁了(2009年诞生),它的设计决策在当时是合理的,但现在成了"技术债务": 安全问题&#x…

2026/7/24 17:15:13 阅读更多 →
异步代码中的时序侧信道防护:常时比较、缓存行填充与 speculative execution 缓解

异步代码中的时序侧信道防护:常时比较、缓存行填充与 speculative execution 缓解

异步代码中的时序侧信道防护:常时比较、缓存行填充与 speculative execution 缓解 一、异步运行时引入的新攻击面 异步编程模型在提升吞吐量的同时,创造了新的侧信道向量。tokio::select! 的分支选择时间、任务调度的唤醒延迟、Arc 引用计数操作的 CPU 周…

2026/7/24 17:15:13 阅读更多 →
Claude Code 实战到底解决了什么问题?

Claude Code 实战到底解决了什么问题?

聊《一次Claude Code项目复盘,问题最后出在流程而不是模型》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。摘要之前团队引入 AI 编程工具时,我们踩过一个典型的坑:Demo 阶段跑通…

2026/7/24 17:15:13 阅读更多 →

日新闻

用Highcharts 创建可拖拽三维散点立方体3D图表

用Highcharts 创建可拖拽三维散点立方体3D图表

该案例基于Highcharts scatter3d 三维散点图实现空间立方体散点可视化,核心特色:三维 X/Y/Z 三轴空间,所有散点分布在 0~10 立方体空间内;散点使用径向渐变实现立体 3D 圆球质感;支持鼠标 / 触屏拖拽画布,…

2026/7/24 0:00:29 阅读更多 →
AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口 AppCertDlls 位于 HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls。本文的程序功能是只读列出这个键在 64 位和 32 位注册表视图中的全部值,并显示每条值的来源、名称、类型和可安全显示的数…

2026/7/24 0:00:29 阅读更多 →
我的编程之路:第一篇博客

我的编程之路:第一篇博客

大家好,我是一名编程初学者,同时这也是我编程学习之路上的第一篇博客。在这里,我想要向大家介绍我的一些想法和规划。a.自我介绍我是一个刚刚接触编程的新手,目前在学习c语言,我对编程世界充满了强烈的好奇。当然&…

2026/7/24 0:00:29 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/24 3:59:20 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/24 1:23:39 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻