本文将从加载、读、写、线程模型、一致性、异常恢复六个维度逐个拆源码。SP 基于 AOSP 的 SharedPreferencesImpl,DataStore 基于 androidx.datastore 的 SingleProcessDataStore。
一、整体架构对比
| SharedPreferences | DataStore | |
|---|---|---|
| 核心类 | SharedPreferencesImpl | SingleProcessDataStore(实现DataStore接口) |
| 并发模型 | 锁 + 内存 Map,读写都在调用线程 | Actor 模型(SimpleActor<Message>),所有读写请求串行排队 |
| 读 API | 同步返回(getString()) | Flow<T>,冷流,挂起函数 |
| 写 API | Editor.commit()/apply() | suspend fun updateData(transform: (T) -> T)/edit {} |
| 存储格式 | XML(XmlUtils.writeMapXml) | Preferences 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"}// 原子替换| 维度 | SP | DataStore |
|---|---|---|
| 写调度 | 调用线程改内存 + 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):Ttransform 收到的是当前已落盘的最新快照,它返回的新值会被原子写回。由于 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,是从根上重做的方案。