ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SharedPreferences、MMKV、DataStore 全面对比与迁移实战

SharedPreferences、MMKV、DataStore 全面对比与迁移实战 做 Android 开发这么久几乎每个项目都会遇到同一个问题登录 token、功能开关、界面偏好这些零散的小数据到底该往哪儿存SharedPreferences 用了好多年后来腾讯开源了 MMKVGoogle 又在 Jetpack 里推出了 DataStore。我最初也觉得无非是换了个接口名真到自己动手迁移、压测、踩坑之后才发现这三者从底层原理到使用体验的差距比想象中大得多。这篇就结合我的实际项目经验把三种主流本地键值存储方案从头到尾对比一遍把各自的原理、上手指南、坑点和适用场景都交代清楚。1. 存储方案选型之前先搞清楚你到底要存什么1.1 键值存储的典型使用场景Android 里的存储方案看着多但每种方案都有自己最适合的战场。键值存储解决的是“少量、零散、高频读写的小数据”这一特定需求。我在实际项目里最常见的几类登录态相关token、uid、账号类型、用户的 UI 偏好主题色、字体大小、列表排序方式、功能开关新功能灰度、公告已读状态、搜索历史这类临时数据。这些数据本身通常只有几十字节到几 KB但读写频率很高而且直接影响启动速度和页面渲染速度。这类数据如果丢进 SQLite 或者 Room也不是不行但要建表、写 DAO、处理升级明显大材小用如果直接写在普通文件里序列化、并发读写、异常恢复全部要自己做代码复杂度一下就上去了。所以 Android 从一开始就提供了 SharedPreferences 这种“开箱即用”的键值存储。后来出现的 MMKV 和 DataStore也都是在这个定位上做差异化只是各自的底层实现和设计哲学完全不同。1.2 选型前要明确的四个维度我做技术选型一般会先列四个问题一是读写性能能不能满足主线程无阻塞的要求二是数据安全性怎么样进程被杀、磁盘写一半会不会丢数据或者坏文件三是使用成本接入复杂度、API 侵入性、是否依赖额外框架四是团队技术栈项目是不是已经上了协程、Jetpack 全家桶有没有跨进程读写需求。这四个问题问完之后再去对比方案就有章法了。下面我会顺着这三个方案各自的底层原理去讲把“为什么快”“为什么慢”“为什么坑”都交代清楚。2. SharedPreferences老牌方案为什么既好用又让人头疼2.1 它的工作原理决定了它注定不完美SharedPreferences 的本质就是一个 XML 文件存放在/data/data/包名/shared_prefs/目录下文件名就是你传入的 name 参数。第一次读取某个 Sp 文件时系统会把整个 XML 一次性加载进内存之后所有 get 操作都走内存所有 put 操作会先改内存再通过 apply 或 commit 写回磁盘。apply 和 commit 的区别是很多新手容易忽略的apply 是异步落盘把修改先提交到内存然后在后台线程写入 XML方法本身立即返回commit 是同步写入会阻塞调用线程直到磁盘操作完成。从单次调用看 apply 体验更好但 apply 有一个隐藏问题——它提交的任务会进入一个名为 QueuedWork 的队列在 Activity 的 onPause、onStop 等生命周期节点系统会等待这个队列里的任务全部执行完。如果写入任务阻塞在 I/O 上就会导致页面切换时 ANR这几乎是 SharedPreferences 最大的性能原罪。// 初始化 SharedPreferences sp getSharedPreferences(user_config, Context.MODE_PRIVATE); // 读取 String token sp.getString(token, ); boolean firstLaunch sp.getBoolean(first_launch, true); // 写入 sp.edit().putString(token, token).putBoolean(first_launch, false).apply();这段代码用起来确实简单但问题也藏在简单里每次 edit 都会生成一个 Editor 对象每次 apply 都会把整个 sp 文件重新序列化并写入哪怕只是改了一个布尔值。如果这个文件里存了几百个 key写入成本会明显放大。2.2 真正的问题到底出在哪第一个问题是全量写入。SharedPreferences 每次提交都会把文件里的所有键值对重新写一遍数据量小的时候无所谓一旦 key 多了就会明显变慢。第二个问题是主线程卡顿我刚才提过的 QueuedWork 等待机制会让看似异步的 apply 依然拖慢页面切换。第三个问题是类型不安全用一个 int 值存进去又用 getString 取出来运行时会直接抛 ClassCastException只能靠人肉保证 key 的规范团队协作时非常容易翻车。第四个问题是跨进程不被支持虽然 MODE_MULTI_PROCESS 这个常量存在但它只保证每次重新加载文件不保证数据一致性官方自己都不推荐用。第五个问题是数据变化无法方便地监听只能借助 OnSharedPreferenceChangeListener回调时机和生命周期绑定都比较别扭用起来不够现代。2.3 什么时候继续用 SP 也不丢人说了这么多问题SharedPreferences 也不是一无是处。如果你的项目是纯 Java 老项目、数据结构非常简单、key 数量控制在几十个以内、没有跨进程需求、也没有明显的主线程卡顿问题那继续用 SP 完全没问题。毕竟它稳定、兼容性最好、所有 Android 版本都支持替换方案本身也有成本。我见过很多团队为了“潮流”强行迁移结果引入新依赖、改了一堆代码后反而出现新问题。存储方案迁移必须带着明确目标比如你已经实际观测到了 ANR、或者有跨进程同步需求否则不要为了换而换。3. MMKV基于 mmap 的高性能方案3.1 先理解 mmapMMKV 是腾讯 2018 年开源的高性能键值组件核心思路是使用 mmap 内存映射技术。mmap 是操作系统提供的一种文件映射机制它把磁盘上的一个文件直接映射到进程的虚拟地址空间里程序读写这块内存就等同于读写文件。数据写入后由操作系统在合适的时机把脏页刷回磁盘对应用来说“写内存”和“写文件”的界限被模糊了。这个机制带来的直接好处是写入极快因为它不需要每次把完整的序列化结果写到磁盘只需要写映射出来的内存页。操作系统自己的脏页回刷机制会保证数据最终落盘即使进程中途被杀已映射部分的数据还留在页缓存里重启后大概率还能读出来。MMKV 还把每个 value 做了单独的 CRC 校验单个 key 的数据损坏不会导致整个文件不可读这一点比 SharedPreferences 健壮不少。简单类比的话SharedPreferences 每次写文件像把整本书重新抄一遍再交上去而 MMKV 就像直接在书的对应页面上改内容速度差距自然明显。3.2 MMKV 的优势和使用方式MMKV 的优点可以总结为四个字快、稳、简、跨。快体现在读写性能普遍比 SharedPreferences 高一个数量级以上尤其是在写入频繁的场景稳体现在自动容错和损坏自恢复能力简体现在接入简单只需要在 Application 里初始化一次跨体现在天然支持多进程同步内部用文件锁实现开箱即用。它还支持把整份数据导出为文件做备份或者从一个文件导入在热修复、插件化场景下非常好用。接入代码很简单第一行初始化之后用法跟 SharedPreferences 几乎一样所以迁移成本很低// Application.onCreate 里初始化 MMKV.initialize(this) // 获取默认实例 val mmkv MMKV.defaultMMKV() // 读写 mmkv.encode(token, abc123) val token mmkv.decodeString(token, )支持的数据类型包括 boolean、int、long、float、double、String、ByteArray、SetString基本覆盖了日常需求。另外 MMKV 的体积控制做得不错使用 protobuf 编码每个 key-value 按序存储不像 XML 那样有大量标签冗余。3.3 MMKV 的使用边界和坑再好的工具也有边界。MMKV 有几个点我实际使用中体会很深。第一跨进程锁虽然稳定但多进程同时高频写入时性能下降明显如果你真有大量多进程并发写需要评估业务频率。第二第一次启动时 mmap 文件会预先分配一块空间文件大小会略大于实际数据很在意存储占用的场景要留意。第三它是腾讯开源的项目虽然背靠微信大规模使用验证但毕竟不是 Android 官方框架团队对此要有心理预期。第四如果使用不当比如存入超大对象或者不合理地高频写入依然会产生 IO 压力。第五数据迁移场景要提前规划旧版本升级到新版本时如果没有统一的初始化逻辑可能出现在旧 SP 里的数据“消失”的错觉——这个我在后面迁移部分会详细说。4. DataStoreGoogle 官方给出的新答案4.1 Preferences DataStore 和 Proto DataStore 的区别DataStore 是 Google 在 Jetpack 里推出的数据存储方案用 Kotlin 协程和 Flow 重新设计了键值存储的 API目的是彻底替代 SharedPreferences。它有两个变体Preferences DataStore 和 Proto DataStore。Preferences DataStore 跟 SharedPreferences 一样以 key-value 形式存数据适合不太在乎数据结构、只想快速上手的场景Proto DataStore 基于 Protocol Buffers 定义 schema可以存强类型的对象适合有明确数据模型、需要类型安全和向后兼容的场景。两者的底层都是同一个 DataStore 核心只是序列化层不同。我第一次接入的时候最大的感受是“别扭”——所有读写都变成了 Flow 和挂起函数没法像 SP 那样拿一个实例直接同步 get。但这种设计是有意的它迫使你把存储操作放到协程里天然避开了主线程 I/O。读取时通过 flow.collect 或者 first() 拿到当前值写入时调用 suspend 的 edit 方法。4.2 DataStore 的优势DataStore 最大的价值在于设计理念。它基于事务处理机制确保读写一致性所有操作都跑在 DataStore 内部的单线程 Dispatcher 上不会阻塞主线程。因为暴露的是 Flow你可以用 collect、map、combine 等操作符组合数据流UI 层可以根据数据变化自动刷新这对 Jetpack Compose 项目和 MVVM 架构非常友好。它还能和 WorkManager、协程结构化并发天然配合。从 Android 官方视角看SharedPreferences 已经处于“不推荐”状态DataStore 就是官方给出的迁移终点。Preferences DataStore 的使用方式如下// 依赖 // implementation(androidx.datastore:datastore-preferences:1.1.1) val Context.userConfigDataStore by preferencesDataStore(name user_config) suspend fun saveToken(token: String) { context.userConfigDataStore.edit { prefs - prefs[TOKEN_KEY] token } } val tokenFlow: FlowString? context.userConfigDataStore.data.map { prefs - prefs[TOKEN_KEY] }看着比 SP 复杂但换来的是数据流响应和天然异步。如果项目已经全面拥抱协程和 Flow这个成本完全值得。4.3 DataStore 的坑点与注意事项DataStore 也不是银弹有几个坑我在项目里踩过。第一数据文件损坏时DataStore 默认会抛异常首次启动如果碰到文件损坏甚至会直接导致崩溃你需要配置 corruptionHandler 做损坏恢复。第二它的读写频率不如 MMKV 激进内部有合理的文件合并机制但如果一个 DataStore 文件里塞了太多无关数据、又频繁写不同 key会让文件膨胀和合并开销变大所以职责单一原则在这里同样适用建议按业务拆多个 DataStore 文件。第三Proto DataStore 引入 protobuf 编译插件后构建配置会变复杂如果团队不熟悉 protobuf 语法前期学习成本不小。第四DataStore 默认不要用于跨进程场景不支持多进程访问。第五同步读取没有官方支持某些极端情况比如开机初始化必须在主线程同步拿值会很难受。5. 三方案横向对比与选型建议为了直观我把三个方案的核心维度整理成了下面的表格对比维度SharedPreferencesMMKVDataStore底层实现XML 全量读写mmap 内存映射 protobuf文件 事务 Flow写入性能低全量序列化高内存映射中异步事务主线程安全有 ANR 风险读写很快但建议异步天然异步不阻塞多进程支持不支持支持不支持类型安全弱一般Proto 强Preferences 一般数据损坏恢复差易整文件损坏好CRC 校验需配置 corruptionHandlerAPI 风格同步Java 老风格同步接近 SPFlow 挂起函数依赖系统框架第三方库Jetpack Kotlin 协程学习成本极低低中官方地位旧方案第三方首选官方新标准我从实际项目角度给出的选型建议是这样的老项目没大问题就继续用 SP不要瞎折腾如果遇到 SP 的 ANR 问题或者明确需要多进程读写直接上 MMKV 是最省事的如果是新项目团队已经用 Kotlin 协程 MVVM那优先选 DataStore因为它是官方推荐的方向未来维护成本低。对追求极致性能、且业务确实有高频写入的场景MMKV 依然是最优解。没有哪个方案在所有维度都赢关键是匹配项目现状。6. 迁移实操从 SP 到新方案的完整路径6.1 从 SharedPreferences 迁移到 MMKV迁移到 MMKV 最舒服的一点是 API 接近逻辑几乎不用重写。我的做法是在 Application 初始化 MMKV 后写一个迁移工具类读取旧 SP 里所有的键值对写入 MMKV然后删除旧的 SP 文件fun migrateFromSP(context: Context, spName: String, mmkv: MMKV) { val sp context.getSharedPreferences(spName, Context.MODE_PRIVATE) val all sp.all ?: return all.forEach { (key, value) - when (value) { is String - mmkv.encode(key, value) is Boolean - mmkv.encode(key, value) is Int - mmkv.encode(key, value) is Long - mmkv.encode(key, value) is Float - mmkv.encode(key, value) is Set* - mmkv.encode(key, value.filterIsInstanceString().toSet()) } } sp.edit().clear().commit() }注意一个细节SetString 在 SP 里内部存储用的是 HashSet从 MMKV 读出来时不要依赖它的顺序。另外迁移完成后建议加一个版本标记只有旧版本第一次升级时才跑迁移逻辑否则每次启动都会重复执行白耗性能。6.2 从 SharedPreferences 迁移到 DataStoreDataStore 官方提供了现成的迁移 API用 SharedPreferencesMigration 就能直接把旧数据搬过来不需要自己遍历键值对val Context.dataStore by preferencesDataStore( name user_config, produceMigrations { context - listOf(SharedPreferencesMigration(context, user_config_sp)) } )这里要注意迁移产生的数据会在 DataStore 首次成功读文件后自动生效之后旧 SP 文件里的修改不会再同步到 DataStore。如果业务有“迁移后还需要旧逻辑回写 SP”的情况一定要先切干净再发布避免两边数据背离。迁移过程中可能出现旧类型与 Preferences 类型不匹配的问题比如 SP 里存了 SetString 但也可能混入其他序列化类型建议迁移前先清理掉不规则数据。6.3 我建议的顺序我个人迁移的经验是“先加后删”四步走第一步新版本同时写 SP 和 NewStore 一份线上观察数据一致性第二步确认 NewStore 数据稳定后改为只读 SP、只在 NewStore 写入第三步去掉 SP 初始化保留一次性的搬迁逻辑第四步发布一段时间后彻底删除 SP 相关代码。这样每个阶段出了问题都能回滚不用赌一次发布把所有事情做完。7. 常见问题与排查技巧实录下面这些问题都是我在真实项目里遇到过的整理成速查表希望能帮你少走弯路。现象可能原因排查与解决方案页面切换 ANRSP apply 触发的 QueuedWork 等待用 MMKV 替换或把写入放到子线程减少主线程生命周期中的写入数据读出来是旧值多进程同时读写 SP换成 MMKV或统一走一个进程维护数据MMKV 数据“消失”旧 SP 数据未迁移、初始化时机不对、跨进程使用不同实例检查 Application 初始化确保迁移逻辑只跑一次DataStore 启动崩溃数据文件损坏配置 corruptionHandler捕获 IOException 并删文件重建Proto DataStore 反序列化失败schema 变更不兼容新增字段要用 optional 或默认值禁止删除已发布字段存储文件越来越大大量 key 高频写入拆分多个 DataStore/MMKV 文件按业务隔离数据我特别想强调一个容易被忽略的坑DataStore 的 corruptionHandler 一定不能省。官方示例里经常不写但实际生产环境里应用被系统强杀、或者存储空间不足时数据文件是可能写坏的。没有 corruptionHandler 的话用户下次启动很可能直接闪退。比较稳妥的做法是启动时捕获异常并重建文件如果数据重要再做本地备份。MMKV 那边虽然自带了 CRC 校验团队里还是要约定好异常处理策略某些极端情况下损坏的字段会被跳过如果业务逻辑对缺数据零容忍要考虑兜底方案。另外在把 DataStore 接入 Compose 项目时我建议把数据流封装在 Repository 层不要让 UI 直接 collect 一堆裸 key。比如做一个 UserPreferenceRepository对外只暴露语义化的 Flow内部管理 key 的读写这样后续换存储方案只需要改 Repository 内部实现上层 UI 完全无感。我在项目里就是这样组织的后来从 SP 迁 MMKV 时UI 层一行代码都没动。最后分享一点个人体会。存储方案这个东西没有绝对的最好只有适不适合当前项目。SharedPreferences 陪伴了我们很多年它的各种问题不是第一步就暴露出来的而是随着业务膨胀才逐渐显现。MMKV 用起来确实爽但要注意它的第三方属性和多进程边界。DataStore 是官方指明的方向API 设计的先进性和与协程的融合度都很高但也别低估了它的学习成本和文件损坏问题。我在实际项目里的原则是能不动就不动要动就带着目标和回滚计划动。希望你也能在真正理解了这三个方案的底层逻辑之后做出最适合自己项目的决定。选型这件事慢就是快。
返回列表