news 2026/7/18 0:38:26

Android随笔-DataStore VS SP

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android随笔-DataStore VS SP

本文将从加载、读、写、线程模型、一致性、异常恢复六个维度逐个拆源码。SP 基于 AOSP 的 SharedPreferencesImpl,DataStore 基于 androidx.datastore 的 SingleProcessDataStore。

一、整体架构对比

SharedPreferencesDataStore
核心类SharedPreferencesImplSingleProcessDataStore(实现DataStore接口)
并发模型锁 + 内存 Map,读写都在调用线程Actor 模型SimpleActor<Message>),所有读写请求串行排队
读 API同步返回(getString()Flow<T>,冷流,挂起函数
写 APIEditor.commit()/apply()suspend fun updateData(transform: (T) -> T)/edit {}
存储格式XML(XmlUtils.writeMapXmlPreferences DataStore 用protobuf 二进制PreferencesSerializer

最大的思想差异:SP 是"共享内存 + 异步落盘",DataStore 是"单写者 Actor + 消息驱动状态机"

二、加载流程

SP:构造即异步加载,读时阻塞等待

// SharedPreferencesImpl 构造函数SharedPreferencesImpl(Filefile,intmode){mFile=file;mBackupFile=makeBackupFile(file);mMap=null;startLoadFromDisk();// 构造时就开子线程加载}privatevoidstartLoadFromDisk(){synchronized(mLock){mLoaded=false;}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(){// ... 拿到锁后valdata=serializer.readFrom(input)// 挂起,IO 线程执行,不阻塞主线程updateData(data)// 状态机从 UnInitialized → Data}

线程从始至终不"等",只是协程挂起恢复——这是二者读路径的本质区别。

三、写流程(核心差异所在)

SP:内存先提交,磁盘写塞进 QueuedWork

apply() 的完整链路:

// EditorImpl.apply()publicvoidapply(){finalMemoryCommitResultmcr=commitToMemory();// ① 内存:同步改 mMap,立即可见finalRunnableawaitCommit=...;// 等磁盘写完的 RunnableQueuedWork.addFinisher(awaitCommit);// ② 注册一个 finisher(ANR 的伏笔)...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 内部str=newFileOutputStream(mFile);XmlUtils.writeMapXml(mapToWriteToDisk,str);// 全量重写,改 1 个 key 也是整文件重写

文件 100KB、每秒写 10 次,就是每秒 1MB 的 IO。

DataStore:Actor 串行处理 UpdateMessage

DataStore 的写是一条消息驱动的链路:

// SingleProcessDataStore.updateData()overridesuspendfunupdateData(transform:suspend(t:T)->T):T{valack=CompletableDeferred<T>(coroutineContext[Job])valupdateMsg=Message.Update(transform,ack,...)actor.offer(updateMsg)// ① 把"写请求"作为消息投给 Actorreturnack.await()// ② 挂起等待这个写完成}

SimpleActor 内部是一个 Channel + 消费协程,所有读写消息在一个协程里严格串行执行

privatesuspendfunconsumeMessages(){for(msginmessageQueue){// Channel 循环消费,天然串行msg.message.runTasks(...)// 按到达顺序依次执行读任务/写任务}}

写文件时用writeScope先写临时文件再原子 rename

// FileConnections.writeScopevalscratchFile=File(file.absolutePath+SCRATCH_SUFFIX)// .tmp 临时文件writeTo(scratchFile){outputStream->serializer.writeTo(newData,outputStream)// protobuf 序列化写入}check(scratchFile.renameTo(file)){"Unable to rename$scratchFile"}// 原子替换
维度SPDataStore
写调度调用线程改内存 + QueuedWork 异步线程落盘Actor 单协程串行处理所有消息
写文件方式全量 XML 重写全量重写,但走.tmp+renameTo原子替换
主线程风险点QueuedWork.waitToFinish()(生命周期回调里)无(全部是挂起,不占线程)
多次写的顺序mDiskWritesInFlight计数 + 版本号保证Channel 天然 FIFO 顺序

四、线程模型对比

SP 的线程足迹

读:调用线程(可能 awaitLoadedLocked 阻塞) 写内存:调用线程 写磁盘:QueuedWork 的单线程 HandlerThread(异步) 隐患:waitToFinish() 在 handleStopActivity 等时机把磁盘 IO 拉回主线程

DataStore 的线程足迹

读:Flow 收集所在协程;文件 IO 在 readScope(Dispatchers.IO) 写:updateData 挂起;Actor 消费消息 + 文件 IO 都在 Dispatchers.IO 主线程:只做 collect,全程零阻塞

一句话:SP 是"线程同步"思维(锁、wait/notify),DataStore 是"协程同步"思维(挂起、Channel、Actor)。前者在错误的线程调用就出事,后者从机制上把 IO 摁死在后台调度器。

五、一致性与事务

SP:单文件内一致,跨进程不可靠

  • 单次 commit/apply 的多个 put 是一次 commitToMemory() 原子替换内存快照,单进程内一致;
  • 用 MemoryCommitResult.memoryStateGeneration 版本号保证"apply 的顺序 == 落盘顺序";
  • 但内存缓存是进程级的,多进程下 A 进程写完,B 进程的内存 Map 毫不知情

DataStore:updateData 就是事务

suspendfunupdateData(transform:suspend(T)->T):T

transform 收到的是当前已落盘的最新快照,它返回的新值会被原子写回。由于 Actor 串行执行消息,两个并发 updateData 天然排队:

// 并发安全,最终一定是 count == 2(SP 里同样的 read-modify-write 需要自己加锁)dataStore.updateData{prefs->prefs.copy(count=prefs.count+1)}dataStore.updateData{prefs->prefs.copy(count=prefs.count+1)}

读侧拿到的永远是 transform 完成后的快照,不存在读到"写一半"的中间态。SingleProcessDataStore 从名字就声明了边界:单进程内强一致,多进程依然是禁区(需要 MultiProcessDataStoreFactory 的方案)。

六、异常与数据损坏恢复

SP:几乎没有恢复手段

XML 解析失败 → loadFromDisk 里 catch 后把 mMap 置为空 Map,原文件数据全部丢失,最多靠 .bak 备份文件(只在上次写成功时存在)。

DataStore:CorruptionException + 可插拔的恢复策略

// 读文件抛 CorruptionException 时,交给 handler 决定恢复值valdataStore=DataStoreFactory.create(serializer=MySerializer,corruptionHandler=ReplaceFileCorruptionHandler{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 数据一次性搬进 DataStore:

valContext.dataStorebypreferencesDataStore(name="settings",produceMigrations={context->listOf(SharedPreferencesMigration(context,"old_sp_name"))})

迁移只执行一次,成功后 SP 文件里的 key 被清掉。注意迁移映射默认是全量迁移,只迁部分 key 要自己传 keysToMigrate。

九、源码级结论

SP 的问题不是 API 老,而是它的同步读 API决定了必须"阻塞或全量内存常驻",QueuedWork 的异步写决定了 ANR 风险内生于框架。DataStore 用协程挂起替代线程阻塞、用 Actor 串行替代锁竞争、用 protobuf + 原子 rename 替代 XML + bak,是从根上重做的方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 0:37:19

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

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

作者头像 李华
网站建设 2026/7/18 0:28:50

音频 AI 智能管理中心主机落地应用指南

在处理企业级音频数据时&#xff0c;最让人头疼的往往不是技术本身&#xff0c;而是数据的“脏乱差”。想象一下这样的场景&#xff1a;客服部门的电话录音、会议室的智能设备采集、外勤人员的移动端上传&#xff0c;这些音频来源各异&#xff0c;格式从 WAV、MP3 到私有编码应…

作者头像 李华
网站建设 2026/7/18 0:22:52

3步掌握GBFR Logs:碧蓝幻想Relink伤害统计工具的完整指南

3步掌握GBFR Logs&#xff1a;碧蓝幻想Relink伤害统计工具的完整指南 【免费下载链接】gbfr-logs GBFR Logs lets you track damage statistics with a nice overlay DPS meter for Granblue Fantasy: Relink. 项目地址: https://gitcode.com/gh_mirrors/gb/gbfr-logs G…

作者头像 李华
网站建设 2026/7/18 0:20:52

JDK详解:从入门到精通

一.什么是jdkJDK也就是Java开发工具包, 它身为整个JAVA的核心, 涵盖了Java运行环境, 也就是Java , 还有一堆诸如javac/java/jdb等的Java工具, 以及Java基础的类库, 也就是Java API包括rt.jar。JDK也就是java开发工具包, 于其安装目录之下存在五个文件夹, 以及一些描述文件, 还有…

作者头像 李华
网站建设 2026/7/18 0:20:18

2026香港EMBA性价比榜单:有海外模块的香港EMBA,民企老板择校避坑测评

【客观测评声明】本文为独立商科教育测评内容&#xff0c;无硬性广告植入&#xff0c;基于民企创始人、科创企业实控人真实就读需求&#xff0c;从成本、落地、圈层、长期赋能四大维度&#xff0c;客观拆解2026香港主流带海外模块的EMBA项目适配度与短板&#xff0c;为企业家择…

作者头像 李华
网站建设 2026/7/18 0:05:38

自动驾驶端到端研发范式切分:从黑箱训练到白盒演进

1. 项目概述&#xff1a;为什么“端到端算法研发范式切分”是当前自动驾驶落地的关键卡点“自动驾驶端到端 算法研发范式切分”这个标题乍看像一句技术黑话&#xff0c;但拆开来看&#xff0c;它直指行业最真实、最紧迫的工程矛盾——不是模型能不能跑通&#xff0c;而是研发流…

作者头像 李华