Android随笔-DataStore VS SP

📅 发布时间:2026/7/18 0:38:43
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是从根上重做的方案。