尧图网络 高端网站定制 · 原创设计
免费咨询热线
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/11/9 10:30:24
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/10/11 13:56:10
韩妆走向全球:韩国创新重塑美妆增长,销售额飙升53%

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

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

📅 2026/10/6 1:12:58
MORE NEWS

更多资讯

📰

滨海高压电工证良心建议:实操考试心里没底怕挂科?看这篇就够了

滨海高压电工证良心建议:实操考试心里没底怕挂科?看这篇就够了 实操考试心里没底怕挂科?你不是一个人。尤其是在当前高压电工证报考门槛和考试要求不断升级的背景下,很多准备报考滨海高压电工证的朋友都面临着同样的焦虑。尤其是对于鹤壁中小施工企业负责…

📰

略阳县报考电工证内部消息:外地电工证怎么转?本地人怎么考?

略阳县报考电工证内部消息:外地电工证怎么转?本地人怎么考? 之前考的证在外省能不能转过来,这事儿很多人问,其实 内部消息 透露,只要你是合法取得的电工证,不管在哪儿考的,只要符合国家规定的条件,是可以 异地转入…

📰

略阳县报考电工证全国通用吗外省证怎么转过来

略阳县报考电工证全国通用吗外省证怎么转过来 之前考的证在外省能不能转过来?这是很多在外地打工的小伙伴最关心的问题,特别是那些在许昌市从事市政公用工程的电工朋友。你们可能会想,我在外地考的电工证,回略阳县还能用吗?别急,下面我来详细说说,略阳…

📰

临河哪里学电工证千万别踩坑,学历不高也能轻松上岸

临河哪里学电工证千万别踩坑,学历不高也能轻松上岸 你是不是也在担心学历不高报不了电工证?别急,这年头谁还没个“门槛”?但别被那些“门槛”唬住, 临河哪里学电工证 ,其实一点都不难。关键是你得找对地方,避开那些 千万别踩坑…

📰

昌平低压电工证复审在哪考?避坑指南与办理全攻略

昌平低压电工证复审在哪考?避坑指南与办理全攻略 不知道去哪报名怕被中介坑?昌平低压电工证复审在哪考?这是很多电工朋友在复审前最担心的问题。尤其是刚入行或者准备转行的电工,对流程和报名渠道不了解,很容易被各种培训机构和中介忽悠,不仅浪费时间,…

📰

上海电工证复核到哪里?真实经历告诉你怎么避开中介坑

上海电工证复核到哪里?真实经历告诉你怎么避开中介坑 不知道去哪报名怕被中介坑?别急,听我一个在电工证复核上踩过坑的老电工说说门道。去年我准备给公司的一批电工办复核,结果被几个中介坑得不轻,花了冤枉钱还差点耽误了工期。这事儿我到现在想起来都后…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

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

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

📞 💬