Android存储方案对比(SharedPreferences 、 MMKV 、 DataStore)

简介:本文介绍了Android开发中常用的键值对存储方案,包括SharedPreferences、MMKV和DataStore,并且对比了它们在性能、并发处理、易用性和稳定性上的特点。通过实际代码示例,帮助开发者根据项目需求选择最适合的存储方案,提升应用性能和用户体验。

 在Android开发中,键值对存储(Key-Value Strorage)是一种经常用到的轻量级数据存储方案。它用于保存一些简单的配置数据或状态信息,例如用户设置、缓存数据等。

常见的键值对存储方案

SharedPreferences

SharedPreferences是Android系统提供的一种轻量级的持久化存储类,使用键值对的形式保存数据;可以存储的数据类型包括String、int、boolean、float和long;简单易用,但在高并发写操作下性能较差,会造成主线程阻塞问题。

SharedPreferences sharedPreferences = getSharedPreferences("my_preferences", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("username", "John");
editor.putInt("age", 30);
editor.apply();  // apply() 异步保存数据,commit() 是同步操作

特点:

存储方式:键值对(Key-Value Pairs),数据以XML文件储存。

使用方式:通常用于存储简单的用户设置、偏好设置等,适合存储少量的数据。

线程安全:在多线程的环境中操作时,需要特别注意同步问题。SharedPreferences本身是线程安全的,但你在修改数据时,可能需要使用apply()或commit()进行操作。

性能:性能较差,尤其是当存储的数据量较大时,因为它是基于文件的,数据时以文本格式存储的,且需要频繁的磁盘IO操作。

 优点:

使用简单,不丢数据

使用非常方便,能确保数据的一致性,适合不频繁读写一些重要的数据。

缺点:

SP不能保证类型安全

获取数据的时候可能出现ClassCastException异常,因为使用相同的KEY调用put()保存不同类型的数据时会覆盖之前保存的数据类型。

SP加载的数据会一直留在内存中

使用getSharedPreferences()方法加载数据会将数据存储在静态的成员变量中,然后通过静态的ArrayMap缓存每一个SP文件,而每个SP文件内容通过Map缓存键值对数据,这样数据会一直留在内存中,浪费内存。

不支持多线程

SP不支持跨进程通信;代码中可以看到当使用多进程MODE_MULTI_PROCESS操作的时候,会重新读取SP文件内容。

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferences sp;
        synchronized (ContextImpl.class){
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if(sp == null){
                checkMode(mode);
                if(getApplicationInfo().tragetSdkVersion >= android.os.Build.VERSION_CODES.O){
                    if(isCredentialProtectedStorage() && !getSystemService(UserManager.class).isUserUnlockingOrUnlocked(UserHandle.myUserId())){
                        throw new IllegalStateException("Credential protected storage is not available");
                    }
                }
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        if((mode & Context.MODE_MULTE_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB){
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it. This has been the historical (if undocumented) behavior.
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

读写性能差,可能引起ANR

 读取数据的时候虽然加载文件也是异步加载的,不过sp.get()方法是同步的,如果代码在它加载完成之前就去尝试读取键值对,线程就会阻塞,直到文件加载完成,此时如果在主线程操作的话,就会造成界面卡顿。

写入数据时SP可以通过apply()异步的方式来保存更改避免I/O操作所导致的主线程的耗时,但当Activity启动和关闭的时候会等待这些异步提交完成保存之后,这就相当于把异步操作转换成同步操作了,从而会导致卡顿甚至ANR。当然这些操作也是为了能保证数据安全一致而为之。

具体ANR引起原因可以参考:剖析 SharedPreference apply 引起的 ANR 问题。

MMKV

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。

总的来说,MMKV使用mmap内存映射文件,极大提高了读写性能,并且支持多进程读写;完全替代SharedPreferences,有一致的API使用体验;提供分布式存储、数据加密等功能。

依赖配置

implementation 'com.tencent:mmkv-static:1.2.10'

初始化和使用

import com.tencent.mmkv.MMKV

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        MMKV.initialize(this)
    }
}

fun saveData(key: String, value: String) {
    val kv = MMKV.defaultMMKV()
    kv.encode(key, value)
}

fun getData(key: String): String? {
    val kv = MMKV.defaultMMKV()
    return kv.decodeString(key)
}
MMKV mmkv = MMKV.defaultMMKV();
mmkv.putString("username", "John");
mmkv.putInt("age", 30);
String username = mmkv.getString("username", "default_value");
int age = mmkv.getInt("age", 0);

MMKV源起

在微信客户端的日常运营中,时不时就会爆发特殊文字引起的系统crash,iOS微信特殊字符保护方案,文章里面设计的技术方案是在关键代码前后进行计数器的加减,通过检查计数器的异常,来发现引起闪退的异常文字。在会话列表、会话界面等有大量cell的地方,希望新加的计数器不会影响滑动性能;另外这些计数器还要永久存储下来---因为闪退随时有可能发生。这就需要一个性能非常高的通用key-value存储组件,考察了SharedPreferences、NSUserDefaults、SQLite等常见组件,发现都没能满足如此苛刻的性能要求。考虑到这个防crash方案最主要的诉求还是实时写入,而mmap内存映射文件刚好满足这种需求,所以尝试通过它实现一套key-value组件。

MMKV原理

内存准备:通过mmap内存映射文件,提供一段可以随时写入的内存块,APP只管往里面写数据,有操作系统负责将内存回写到文件,不用担心crash导致数据丢失。

数据组织:数据序列化方面选用protobuf协议,pb在性能和空间占用上都有不错的表现。

写入优化:考虑到主要使用场景是频繁的写入更新,所以需要有增量更新的能力。因此考虑将增量kv对象序列化后,append到内存末尾。

空间增长:使用append实现增量更新带来了一个新的问题,就是不断append的话,文件大小会增长的不可控。需要在性能和空间上做一个折中。

更详细的设计原理可以参考design · Tencent/MMKV Wiki · GitHub文档。

特点:

存储方式:MMKV基于内存映射文件(Memory-Mapped File)实现,数据存储在磁盘上,但是可以直接从内存访问,避免了频繁的磁盘I/O操作。

性能:相比于SharedPreferences,MMKV提供了更高的性能,尤其是在大量数据存储和访问的场景中表现更好。

加密支持:支持加密,可以加密存储的数据,适用于需要加密保护的场景。

线程安全:MMKV是线程安全的,多线程可以同时访问。

易用性:API使用方式与SharedPreferences类似,迁移成本较低。

优点:

支持多进程

如果需要多进程通信,那暂时就只能用MMKV了。

"快"

单进程性能

MMKV &  SharedPreferences & SQLite读写速度对比:

MMKV 在写入性能上远远超越 SharedPreferences & SQLite,在读取性能上也有相近或超越的表现。

多线程性能

MMKV &  SharedPreferences & SQLite读写速度对比:

可见,MMKV无论是在写入性能还是在读取性能都要远远超越SharedPreferences 和 SQLite,MMKV在Android多进程key-value存储组件上是不二之选。

缺点:

写入大数据速度较慢

当使用MMKV写入大的字符串数据时,相比于SP和DataStore会慢一些,但是开发中基本不会写入那么大的字符串。

可能会丢数据

当设备突然断电关机等意外现象时,刚好数据保存在一半的情况下,此时文件就会发生损坏。这种问题是不可避免的,MMKV的底层机制在断电关机之类的操作系统级别的崩溃,没有做备份还原操作,数据就会损失重置;MMKV底层的原理是内存映射,它不是实时的将内存中的数据写入到磁盘中,会有一定的滞后性,MMKV定位于高频写入可能这就是它不实时写入磁盘的原因吧。

而SharedPreferences和DataStore的应对方式是在每次写入新数据之前都对现有文件做一次自动备份,这样在发生意外出现文件损坏之后,它们机会把备份的数据恢复过来。

 DataStore

Google提供的现代化数据存储解决方案。分为Preferences DataStore 和 Proto DataStore两类,前者也是基于键值对的存储,后者基于ProtoBuf。用Kotlin协程和Flow实现异步、响应式编程;类型安全、无业务侵入,支持直接保存对象。

依赖配置

implementation "androidx.datastore:datastore-preferences:1.0.0"

Preferences DataStore

import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

private val Context.dataStore by preferencesDataStore("settings")

object PreferencesKeys {
    val EXAMPLE_KEY = stringPreferencesKey("example_key")
}

suspend fun saveData(context: Context, value: String) {
    context.dataStore.edit { preferences ->
        preferences[PreferencesKeys.EXAMPLE_KEY] = value
    }
}

fun getData(context: Context): Flow<String?> {
    return context.dataStore.data
        .map { preferences ->
            preferences[PreferencesKeys.EXAMPLE_KEY]
        }
}
// 定义一个 Preferences
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

// 存储数据
val usernameKey = stringPreferencesKey("username")
val ageKey = intPreferencesKey("age")

suspend fun saveData(context: Context) {
    context.dataStore.edit { preferences ->
        preferences[usernameKey] = "John"
        preferences[ageKey] = 30
    }
}

// 获取数据
suspend fun readData(context: Context): String? {
    val preferences = context.dataStore.data.first()
    return preferences[usernameKey]
}

Proto DataStore

// Proto 文件
message UserSettings {
    string username = 1;
    int32 age = 2;
}

// Proto DataStore 用法
val userSettingsDataStore: DataStore<UserSettings> = context.createDataStore(
    fileName = "user_settings.pb",
    serializer = UserSettingsSerializer
)

// 存储数据
suspend fun saveProtoData() {
    userSettingsDataStore.updateData { currentSettings ->
        currentSettings.toBuilder().setUsername("John").setAge(30).build()
    }
}

// 获取数据
suspend fun readProtoData(): UserSettings? {
    return userSettingsDataStore.data.first()
}

特点:

异步操作:DataStore 是完全基于 Kotlin 协程的,操作是异步的,避免了阻塞主线程的问题。

类型安全:支持使用类型安全的 Proto 数据格式来存储结构化的数据,避免了在 SharedPreferences 中手动转换数据类型的麻烦。

迁移支持:从 SharedPreferences 迁移到 DataStore 方便且有帮助,尤其是对于大部分简单的键值对存储需求。

优点:

性能高、不卡顿、不丢数据

DataStroe基于Kotlin协程实现和使用,官方主推性能,主线程读写(不管大小)数据都不卡顿(MMKV读写长字符串时可能会发生卡顿)。

官方站台主推数据存储方案

官方代替SharedPreferences方案,SP有的基础上并优化了性能问题,选择存储方案时应该优先考虑。

缺点:

不支持多进程

暂时不支持多进程。

需要支持KT协程

DataStroe基于Kotlin协程实现和使用,如果你的项目还是纯Java的话,还是用SP忍一忍吧。

总结对比:

特性SharedPreferencesMMKVJetpack DataStore
存储方式键值对,XML 文件键值对,内存映射文件键值对(Preferences),协议缓冲(Proto)
性能较差,尤其是在数据量较大时高效,支持大规模数据高效,支持异步操作
线程安全需要手动同步操作默认线程安全默认线程安全
加密支持不支持支持不支持(需要额外处理)
支持结构化数据存储不支持不支持支持(Proto DataStore)
API 设计简单,传统简单,基于 SharedPreferences 类似更现代,基于 Kotlin 协程和流
使用场景存储简单设置和小型数据高性能存储,尤其适用于需要处理大量数据的场景适用于需要异步操作的场景,支持结构化数据

结论:

  • SharedPreferences:适合存储小型、简单的配置信息,操作简单,适用于老旧项目。
  • MMKV:适用于对性能有较高要求的应用,特别是数据量较大时,性能优越,且支持加密。
  • Jetpack DataStore:推荐用于现代 Android 应用,尤其是需要异步处理或结构化数据存储的场景,支持 Kotlin 协程和流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/949986.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[微服务]redis主从集群搭建与优化

搭建主从集群 单节点Redis的并发能力是有上限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离。 1. 主从集群结构 下图就是一个简单的Redis主从集群结构&#xff1a; 如图所示&#xff0c;集群中有一个master节点、两个s…

vue3 react使用高德离线地图

下载离线资源 下载地址 https://download.csdn.net/download/u010843503/90234612 2、部署私有化瓦片资源 ngxin中配置如下 server{listen 18082;server_name localhost;location / {root D:/GisMap/_alllayers;#try_files $uri $uri/ /index.html;#index index.html;} }下载…

【数据结构-堆】力扣2530. 执行 K 次操作后的最大分数

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你的 起始分数 为 0 。 在一步 操作 中&#xff1a; 选出一个满足 0 < i < nums.length 的下标 i &#xff0c; 将你的 分数 增加 nums[i] &#xff0c;并且 将 nums[i] 替换为 ceil(nums[i] / 3) 。 返回在 恰好…

基于华为ENSP的OSPF状态机、工作过程、配置保姆级别详解(2)

本篇技术博文摘要 &#x1f31f; 基于华为enspOSPF状态机、OSPF工作过程、.OSPF基本配置等保姆级别具体详解步骤&#xff1b;精典图示举例说明、注意点及常见报错问题所对应的解决方法 引言 &#x1f4d8; 在这个快速发展的技术时代&#xff0c;与时俱进是每个IT人的必修课。我…

运动相机拍摄的视频打不开怎么办

3-10 GoPro和大疆DJI运动相机的特点&#xff0c;小巧、高清、续航长、拍摄稳定&#xff0c;很多人会在一些重要场合用来拍摄视频&#xff0c;比如可以用来拿在手里拍摄快速运动中的人等等。 但是毕竟是电子产品&#xff0c;有时候是会出点问题的&#xff0c;比如意外断电、摔重…

gateway的路径匹配介绍

gateway是一个单独服务。通过网关端口和predicates进行匹配服务 1先看配置。看我注解你就明白了。其实就是/order/**配置机制直接匹配到orderservice服务。 2我试着请求一个路径&#xff0c;请求成功。下面第三步是请求的接口。 3接口。

Bytebase 3.1.0 - 通过 Google / GitHub SSO 功能开放给专业版

&#x1f680; 新功能 支持在 PostgreSQL DML/DDL 工单中选择执行角色。 在项目设置中增加 PostgreSQL 数据库租户模式配置选项。 在数据库页面和 SQL 编辑器为 ORACLE 数据库展示 package 元数据。 支持为环境配置颜色&#xff0c;方便区分。 新增管理员可关闭数据导出…

【C++笔记】红黑树(RBTree)深度剖析和AVL树的对比分析

【C笔记】红黑树(RBTree)深度剖析和AVL树的对比分析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;C笔记 文章目录 【C笔记】红黑树(RBTree)深度剖析和AVL树的对比分析前言一.红黑树的定义1.1 红黑树的概念1.2红黑树的规则1.3 红黑树对比A…

(概率论)无偏估计

参考文章&#xff1a;(15 封私信 / 51 条消息) 什么是无偏估计&#xff1f; - 知乎 (zhihu.com) 首先&#xff0c;第一个回答中&#xff0c;马同学图解数学讲解得很形象&#xff0c; 我的概括是&#xff1a;“注意&#xff0c;有一个总体的均值u。然后&#xff0c;如果抽样n个&…

Mac中配置vscode(第一期:python开发)

1、终端中安装 xcode-select --install #mac的终端中安装该开发工具 xcode-select -p #显示当前 Xcode 命令行工具的安装路径注意&#xff1a;xcode-select --install是在 macOS 上安装命令行开发工具(Command Line Tools)的关键命令。安装的主要组件包括&#xff1a;C/C 编…

【顶刊TPAMI 2025】多头编码(MHE)之极限分类 Part 3:算法实现

目录 1 三种多头编码&#xff08;MHE&#xff09;实现1.1 多头乘积&#xff08;MHP&#xff09;1.2 多头级联&#xff08;MHC&#xff09;1.3 多头采样&#xff08;MHS&#xff09;1.4 标签分解策略 论文&#xff1a;Multi-Head Encoding for Extreme Label Classification 作者…

【形式篇】年终总结怎么写:PPT如何将内容更好地表现出来

——细节满满&#xff0c;看完立马写出一篇合格的PPT 总述 形式服务于内容&#xff0c;同时合理的形式可以更好地表达和彰显内容 年终总结作为汇报型PPT&#xff0c;内容一定是第一位的&#xff0c;在内容篇(可点击查看)已经很详细地给出了提纲思路&#xff0c;那如何落实到…

软件项目体系建设文档,项目开发实施运维,审计,安全体系建设,验收交付,售前资料(word原件)

软件系统实施标准化流程设计至关重要&#xff0c;因为它能确保开发、测试、部署及维护等各阶段高效有序进行。标准化流程能减少人为错误&#xff0c;提升代码质量和系统稳定性。同时&#xff0c;它促进了团队成员间的沟通与协作&#xff0c;确保项目按时交付。此外&#xff0c;…

大模型(LLM) 的长上下文与 RAG:评估与回顾

大模型的长上下文与 RAG 以下是本文的主要发现&#xff1a; 在问答基准测试中&#xff0c;LC 的表现通常优于 RAG 基于摘要的检索与 LC 性能相当&#xff0c;而基于块的检索则落后 RAG 在基于对话和一般性问题查询方面具有优势 本文对结果进行了深入分析&#xff0c;请查看。 …

SSR 【1】【nuxt安装】

文章目录 前言如何解决 前言 nuxt提供了nuxi脚手架工具&#xff0c;让开发者便捷生成nuxt模板项目。nuxt官网 npx nuxilatest init <project-name>但是几乎大部分的人在安的时候都会遇到这个问题 如何解决 在C:\Windows\System32\drivers\etc\hosts中增加如下解析记录…

性能测试05|JMeter:分布式、报告、并发数计算、性能监控

目录 一、JMeter分布式 1、应用场景 2、原理 3、分布式相关注意事项 4、分布式配置与运行 二、JMeter报告 1、聚合报告 2、HTML报告 三、并发用户数&#xff08;线程数&#xff09;计算 四、JMeter下载第三方插件 五、性能监控 1、Concurrency Thread Group 线程组…

CURSOR 应用:深入理解字符前缀条件算法(Character Prefix Conditioning)

前言 在代码补全中&#xff0c;用户期待智能模型能根据输入快速、准确地给出建议。但现代语言模型基于Token序列运作&#xff0c;这在处理非Token边界输入时会带来偏差。为了解决这一问题&#xff0c;本文将探讨一种高效算法——字符前缀条件算法&#xff08;Character Prefix…

滤波器设计流程

sos滤波器是什么为什么要 zpk2sos如何实现零相位滤波&#xff0c;优缺点分别是什么 滤波器的计算流程 滤波器的计算设计流程&#xff1a; 1.输入验证和处理&#xff1a; 2.检查频率范围是否合法&#xff0c;计算归一化的频率。 3.滤波器设计&#xff1a;设计带通 Butterworth…

【游戏设计原理】53 - 解决问题的障碍

1. 分析并总结原理 核心观点 游戏本质是一系列问题解决的过程&#xff0c;通过设计巧妙的问题和决策场景&#xff0c;游戏能激发玩家的兴趣和投入感。然而&#xff0c;当问题解决的过程被阻碍时&#xff0c;会降低玩家的体验甚至让他们放弃游戏。文中提到的四种障碍反映了玩家…

【多线程初阶篇¹】线程理解| 线程和进程的区别

目录 一、认识线程Thread 1.为啥引入线程 2.线程理解 &#x1f525; 3.面试题&#xff1a;线程和进程的区别 一、认识线程Thread 1.为啥引入线程 为了解决进程太重量的问题 解释&#xff08;为什么说线程比进程更轻量&#xff1f;/为什么说线程创建/销毁开销比进程小&#…