对比 SharedPreferences 的 XML 与 apply/commit、DataStore 的 Flow 和事务更新、MMKV 的 mmap 与追加写,讲清多进程、性能、持久性和迁移边界。
一个深色模式开关,SharedPreferences、DataStore、MMKV 都能存;但它们解决问题的方式并不相同。SharedPreferences(下文简称 SP)是 Android 平台自带的 XML 键值存储,DataStore 面向异步和响应式的数据更新,腾讯 MMKV 则以 mmap 为核心优化小型键值读写。这里把提问中的“mkkv”按 MMKV 理解。
判断该用谁,不能只看一张“写入速度排行榜”。更重要的是:读写是否会卡主线程?调用返回时数据是否已持久化?是否跨进程?数据是否有结构和事务要求?
一、先看结论:三者分别适合什么
| 主要数据形态 | 少量基础类型键值 | Preferences 键值或 Proto 类型化对象 | 少量基础类型、字符串、字节等键值 |
| 常用 API 风格 | 同步 get;apply / commit 写 | Flow 读;edit / updateData 挂起写 | 同步 decode / encode |
| 典型底层组织 | XML 文件,读取后维护进程内键值 Map,写回文件 | 串行事务更新,序列化存储快照 | 内存映射文件、二进制编码与追加式更新 |
| 多进程 | 不应依赖旧的 MODE_MULTI_PROCESS 获得一致性 | 显式使用多进程 DataStore 工厂 | 显式使用多进程模式 |
| 响应式观察 | 监听器,需自行管理生命周期 | 内建 Flow | 原生读写 API 不是 Flow,需自行包装观察 |
| 依赖 | 平台内置 | AndroidX 库 | 第三方原生库,含 native 代码 |
| 优先考虑的场景 | 维护少量既有配置 | 新项目的用户设置、类型化小型状态 | 高频小键值读写、已有 MMKV 体系或明确的多进程需求 |
上表是选型起点,不是绝对性能排名。三者都不是关系型数据库:列表、搜索、关联、分页或大量结构化记录应考虑 Room 等数据库,而不是不断往一个键值文件里塞 JSON。
二、SP 原理:XML + 进程内键值 Map,为什么 apply() 仍会卡?
val sp = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val enabled = sp.getBoolean("dark_mode", false)
sp.edit().putBoolean("dark_mode", true).apply()
SP 会把 XML 内容读入进程内的键值映射,之后常见的 get 从内存取值。首次访问若文件尚未加载完,调用方仍可能等待加载;“读取平时很快”不代表冷启动完全没有 I/O 成本。
写入时 Editor 先修改内存中的值,再安排磁盘落盘。SP 文件并非为每个 key 单独建索引和增量数据页;一次修改通常需要把当前偏好内容重新写成 XML 文件。平台实现会采取文件备份/替换等措施降低损坏风险,但不能把它理解成可跨多个业务对象的数据库事务。
apply() 与 commit() 的真正区别
| apply() | 调用期间先更新 | 异步排队 | 无成功布尔值 |
| commit() | 调用期间更新 | 调用方等待写入完成 | Boolean |
apply() 返回后,当前进程读到的新值通常已经生效,但不能据此证明磁盘已完成持久化;进程突然终止时有丢失最近更改的风险。commit() 等待磁盘写入,不该在主线程大量调用。apply() 也不是“永远没有主线程成本”:大量待落盘工作可能在生命周期切换等时机等待收尾,造成卡顿甚至 ANR 风险。
SP 的旧多进程模式不提供可依赖的实时一致性。两个进程各自持有一份内存中的键值 Map 时,一个进程更新并不意味着另一个进程立刻看到;新方案不要把它当作跨进程共享数据库。
三、DataStore 原理:串行更新 + Flow + 持久化事务
DataStore 有两种常见形态:Preferences DataStore 类似类型安全 key 的键值设置;Proto DataStore 使用 Protocol Buffers schema 定义完整的数据对象。两者共享 DataStore 的异步更新模型,但 Proto 更适合有稳定结构、需要 schema 演进的小型配置。
private val Context.settingsStore by preferencesDataStore(name = "settings")
private val DARK_MODE = booleanPreferencesKey("dark_mode")
val darkMode: Flow<Boolean> = context.settingsStore.data
.map { preferences -> preferences[DARK_MODE] ?: false }
suspend fun setDarkMode(context: Context, enabled: Boolean) {
context.settingsStore.edit { preferences ->
preferences[DARK_MODE] = enabled
}
}
DataStore 将对同一存储的更新串行处理,在 edit / updateData 的变换中读取当前状态、修改并提交;成功返回意味着这次更新已经由 DataStore 写入其持久存储。data 是 Flow,订阅者可以观察后续状态,而不必手工维护 SP 监听器与线程切换。UI 中应按生命周期收集它,避免重复启动无主收集器。
DataStore 的优势是一致的更新语义和异步 API,不是“每次只修改文件里的一个字节”。Preferences 或 Proto 更新仍涉及对数据对象的序列化和文件写入;文件不断变大、高频写入大量对象时,DataStore 不是数据库的替代品。也不要为同一个底层文件创建多个互相独立的 DataStore 实例。
多进程不是默认自动完成的
需要跨进程读写同一份数据时,应按 AndroidX 的多进程 DataStore API(例如 MultiProcessDataStoreFactory)配置,并保证各进程指向同一文件、使用一致的数据格式。不要对同一文件混用单进程与多进程 DataStore,或同时让 SP/MMKV 去写那份 DataStore 文件。多进程支持解决的是进程间一致更新,不是跨设备同步。
Proto 和 Preferences 怎么选?
- Preferences:少量独立设置项,迁移旧 SP 较直接;key 与值的业务约束仍需自己设计。
- Proto:一组有关联的类型化配置、默认值和 schema 演进要求;需要维护 .proto 与序列化器。
两者都适合小型设置或状态,不是缓存大图片、视频文件和可查询记录的地方。DataStore 还提供从旧 SP 迁移的辅助机制;迁移后应停止继续向旧 SP 写同一业务配置,否则新旧来源会分叉。
四、MMKV 原理:mmap、二进制编码与追加写
MMKV.initialize(context)
val kv = MMKV.mmkvWithID("settings", MMKV.SINGLE_PROCESS_MODE)
kv.encode("dark_mode", true)
val enabled = kv.decodeBool("dark_mode", false)
MMKV 的核心是 memory-mapped file(内存映射文件):通过 mmap 把文件映射到进程地址空间,读写可直接操作映射区域,减少传统文件 I/O 路径上的复制与系统调用。它采用紧凑的二进制序列化和追加式更新,空间不足或无效记录增多时需要扩容、整理/回写;并非“每次修改都重写整份 XML”。
MMKV 还会进行完整性检查,并提供多进程模式下的同步机制。但几个容易被误读的点必须分开:
- **映射到地址空间不等于整个文件一直占用等量物理内存。**页面按需进入内存,也可能发生缺页与磁盘 I/O。
- **encode() 返回不等于每个字节都已同步到持久介质。**脏页的刷盘由系统与库的同步策略共同决定;关键写入应阅读所用 MMKV 版本的同步 API 与崩溃恢复说明。
- **多进程模式不等于业务事务。**多个进程读写需要显式使用对应模式;跨多个 key 的业务不变量仍要自己设计。
- **原生库不是零成本。**初始化、ABI 产物、升级兼容、文件迁移和排查 native 问题都是工程成本。
MMKV 提供可选加密能力,但加密开关并不自动解决密钥保管、轮换、备份或设备迁移问题。登录令牌等敏感数据应先做威胁模型和 Android Keystore 相关设计;SP、DataStore、MMKV 的普通文件形式都不应被当成“天然安全保险箱”。
五、为什么“MMKV 比 SP 快”不能成为唯一理由?
常见基准测试把几万次写入放在同一进程、同一个热缓存环境中,然后只比较 API 调用耗时。这样的测试可能说明该场景的热路径开销,却不能直接推导冷启动、强制持久化、多进程或进程被杀后的表现。
一个更公平的测试至少要固定:同一设备与 Android 版本、数据量和 key 分布、冷读/热读、单次写/批量写、是否等待持久化、进程重启后是否仍能读到、文件变大后的整理成本。DataStore 的挂起式事务、SP 的 apply、MMKV 的普通 encode 若持久化语义不同,直接比较“返回用时”并不公平。
真实项目选型可按这个顺序:
六、迁移时最容易遗漏的事
从 SP 转 DataStore,可利用 SharedPreferencesMigration 在首次访问时读取旧值。迁移要明确哪些 key、缺省值如何解释、旧文件何时删除,以及旧版本组件是否仍写 SP。不要让新页面读 DataStore、旧后台任务继续写 SP,却以为二者自动同步。
从 SP 转 MMKV 也要先规划一次性数据导入、版本标记和失败回退;避免每次启动重复导入,把用户新值覆盖成旧值。无论迁移到哪种存储,都应在真实设备上覆盖“旧版安装 → 升级 → 修改配置 → 杀进程重启”这条路径。
如果已有多进程服务,先画出每个进程实际读写的 key,再决定单进程 DataStore、多进程 DataStore 或 MMKV 多进程模式;不要用一次单进程单测代替跨进程验证。
七、面试高频问答
Q1:apply() 与 commit() 有什么区别? apply 先更新内存、异步安排磁盘写入且不返回成功状态;commit 等待磁盘写入并返回布尔值。两者都不适合毫无节制地在主线程高频写大数据。
Q2:DataStore 为什么能避免常见 SP 卡顿? 它提供挂起式写入、Flow 读取与串行事务更新,调用方不需要在主线程同步等待整份设置写盘;但本身仍会做文件 I/O 和序列化。
Q3:Preferences DataStore 与 Proto DataStore 的区别? 前者按 key 存设置,后者按 protobuf schema 存类型化对象;两者都用 DataStore 的更新与观察机制。
Q4:MMKV 为什么常在小型键值读写中表现快? mmap 减少 I/O 路径开销,二进制编码与追加写避免每次都把 XML 全量重写;具体表现仍受数据量、整理和持久化要求影响。
Q5:MMKV 的 mmap 是否等于不会丢数据? 不是。映射写入与物理介质落盘不是同一时刻;要区分 API 返回、同步刷盘和突然断电等场景。
Q6:三者都支持多进程吗? SP 旧多进程模式不可靠;DataStore 需要显式多进程配置;MMKV 也需要显式多进程模式。即便支持,多 key 业务一致性仍要单独设计。
Q7:为什么不能用它们替代 Room? 它们面向小型键值或配置,不擅长复杂查询、索引、关联及大量记录事务。
一句话收束:**SP 是平台遗留且够用的简单设置方案,DataStore 更看重异步、一致更新与响应式观察,MMKV 更看重小型键值读写路径的效率。**先确认数据语义和可靠性要求,再谈基准测试里的毫秒数。
参考资料
- Android Developers:DataStore
- Android API:SharedPreferences
- Android API:SharedPreferences.Editor
- 腾讯 MMKV 开源项目
- 腾讯 MMKV Wiki
- 本项目:Android 数据库使用指南
网硕互联帮助中心

评论前必须登录!
注册