【Android】Android的键值对存储方案对比

一、背景

键值对的存储在android开发功能时非常常见。如模式的开/关、系统状态、界面状态等,这些很适合用键值对来存。

键值对的存储方案,最常用的就是Android自带的 SharedPreferences。SharedPreferences使用起来很简单,但是却有一个致命的问题--卡顿,甚至有时候会出现ANR。

内部存储框架存在的问题:

  • SP存在跨进程不安全,数据量⼤的时候加载缓慢,全量写⼊可能引起ANR问题
  • 读取SP内容时,由于读取操作是同步的,必须等待SP加载完毕后才能读取,也有可能导致ANR

在top10的ANR中,SP导致的问题占40%

二、SharedPreferences的优劣势:

SharedPreferences是Android平台上用于存储简单数据的轻量级机制。它主要用于保存应用中的偏好设置和配置信息。

优势:

  1. 简单易用:

    • SharedPreferences提供了简单的API,用于存储和检索基本类型的数据(如布尔值、浮点数、整型、长整型和字符串)。
    • 适合存储少量的键值对数据,使用起来非常方便。
  2. 持久性:

    • 数据存储在XML文件中,应用关闭后数据依然存在,不会丢失。
    • 自动处理数据的持久化,开发者无需额外操作。
  3. 访问速度快:

    • 由于数据量小,读取和写入操作速度较快。
    • 适合存储需要频繁访问的小型数据。
  4. 适合存储用户设置:

    • 特别适合保存用户偏好设置,如应用主题、语言选择等。

劣势:

  1. 不适合存储大数据:

    • SharedPreferences主要用于存储小型数据,不适合存储复杂或大型数据结构。
    • XML文件结构限制了其存储能力和性能。
  2. 数据结构单一:

    • 只能存储简单的键值对,无法直接存储对象或列表。
    • 对于复杂数据结构,需要进行序列化处理。
  3. 线程安全问题:

    • SharedPreferences本身是线程安全的,但在多线程环境下进行写操作时,可能需要额外的同步处理。
  4. 加密支持有限:

    • 数据以明文形式存储,敏感信息需要额外处理(如加密)以确保安全。
  5. 性能问题:

    • 在频繁写入大量数据时可能导致性能下降,因为每次写入都会修改底层XML文件。

总体来说,SharedPreferences适用于需要存储简单、少量数据的场景,尤其是用户偏好设置。但对于更复杂的数据存储需求,开发者可能需要考虑使用SQLite数据库或其他数据存储方案。

###基于此,如过考虑替换键值对存储方案,还有两个:MMKV、DateStore

MMKV是腾讯的项目。它和SharedPreferences一样,都是做键值对存储的,可是它的性能比SharedPreferences强很多。

DataStore则是Android官方给出的SharedPreferences的替代品。

三、性能对比

下面是MMKV官方给出的数据对比图,将MMKV和SharedPreferences、SQLite进行对比,重复读写操作1k次。

  • 单进程性能

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

  • 多进程性能
    可见,MMKV无论是在写入性能还是在读取性能,都远远超越MultiProcessSharedPreferences & SQLite & SQLite
    但是SharedPreferences 是有异步API的,而DataStore是基于协程的,而界面的流畅在意的正是主线程的时间消耗,所以如果统计的不是全部的耗时,而是主线程的耗时(单位:ms),如下:

    可以看到MMKV的存储耗时最短,性能更强。但事实上,它并不是任何时候都更强。由于内存映射这种方案是自行管理一块独立的内存,所以它在尺寸的伸缩上面就比较受限,这就导致它在写大一点的数据的时候,速度会慢,而且可能会很慢。如果写入的内容从Int换成长字符串,如下

    这时候MMKV就不具备优势了,反而成了耗时最久的;耗时最短的成了DataStore。
    从最终的数据来看,三种方案都不是很慢,因为这是1000次连续写入的耗时,而在真正的项目中,是不会一次性做1000次的长字符串的写入。所以不管选哪种方案,实际耗时都少到了可以忽略的程度。

四、MMKV优劣势

优势一:写速度极快

从MMKV官方给到的性能对比图中看来,SharedPreferences 的耗时是MMKV的接近60倍。很明显,如果SharedPreferences用异步的API也就是 apply() 来保存的话,是不可能有这么差的性能的,这个一定是使用同步的 commit() 的性能来做的对比。在同步处理的机制下,MMKV的性能优势就很明显,因为它写入内存就几乎等于写入了磁盘,所以速度巨快无比。这就是MMKV的优势之一:极高的同步写入磁盘的性能。

另外MMKV还有个特点是,它的更新并不像 SharedPreferences 那样全量重新写入磁盘,而是只把要更新的键值对写入,也就是所谓的增量式更新。这也会给它带来一些性能优势,不过这个优势并不算太核心,因为 SharedPreferences 虽然是全量更新的模式,但只要把保存的数据用合适的逻辑拆分到多个不同的文件里,全量更新并不会对性能造成太大的拖累。

优势二:支持多进程

SharedPreferences是不支持多进程的,DataStore目前在dataStore1.1.0 alpha版本支持多进程使用,但是稳定版并不支持。

劣势一:丢数据

MMKV虽然由于底层机制的原因,在程序崩溃的时候不会影响数据往磁盘的写入,但断电关机之类的操作系统级别的崩溃,文件照样会损坏。对于这种文件损坏,SharedPreferences和DataStore的应对方式是在每次写入新数据之前都对现有文件做一次自动备份,这样在发生了意外出现了文件损坏之后,它们就会把备份的数据恢复过来;而MMKV,没有这种自动的备份和恢复,那么当文件发生了损坏,数据就丢了,之前保存的各种信息只能被重置。

所以用MMKV,可以用它来存可以接受丢失、不那么重要的数据,对于重要的数据,可以使用其他没有 风险的方案存储。

当然,MMKV的文件损坏终归是个概率极低的事件,但是在使用MMKV的时候,一定要考虑到这个问题。

**

五、MMKV的使用

**

MMKV 是由腾讯微信团队开源的高性能键值对存储组件,它使用内存映射文件(Memory-mapped file)来实现高效的数据读写操作。

添加依赖

复制代码
dependencies {
    implementation 'com.tencent:mmkv-static:1.2.15'
}

初始化,程序启动时候。

复制代码
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 初始化 MMKV
        MMKV.initialize(this)
    }
}

保存数据,支持多种数据类型,包括基本类型(int、string、bool 等)和复杂类型(如 Set)

复制代码
fun saveData() {
    // 获取默认的 MMKV 实例
    val mmkv = MMKV.defaultMMKV()

    // 存储简单的数据类型
    mmkv.encode("intKey", 123)
    mmkv.encode("stringKey", "Hello MMKV")
    mmkv.encode("boolKey", true)

    // 存储复杂的数据类型,例如 Set<String>
    val set = setOf("a", "b", "c")
    mmkv.encode("setKey", set)
}

读取数据,使用decode方法,根据数据的类型选择相应的方法(如 decodeInt、decodeString等)并提供一个默认值以防止空指针异常。

复制代码
fun loadData() {
    // 获取默认的 MMKV 实例
    val mmkv = MMKV.defaultMMKV()

    // 读取简单的数据类型
    val intValue = mmkv.decodeInt("intKey", 0)
    val stringValue = mmkv.decodeString("stringKey", "default")
    val boolValue = mmkv.decodeBool("boolKey", false)

    // 读取复杂的数据类型,例如 Set<String>
    val setValue: Set<String>? = mmkv.decodeStringSet("setKey")

    // 打印读取到的数据
    println("Int Value: $intValue")
    println("String Value: $stringValue")
    println("Bool Value: $boolValue")
    println("Set Value: $setValue")
}

**

六、DataStore的使用

**

DataStore是Android Jetpack库中的一部分,提供了一种新的数据存储解决方案,用于替代SharedPreferences。它支持异步API和数据类型安全,并有两种不同的实现:Preferences DataStore和 Proto DataStore。

Preferences DataStore提供了一种类型安全、异步的方式来存储简单键值对数据。它的异步特性使得它在处理较大的数据集时比SharedPreferences更高效,并且使用flow来读取数据使得它能够自动响应数据变化。

添加依赖:

复制代码
dependencies {
    implementation "androidx.datastore:datastore-preferences:1.0.0"
}

创建实例 :在application或者activity,activity内可使用lifecycleScope.launch启动协程

复制代码
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

保存数据,定义key

复制代码
#定义key
private val EXAMPLE_KEY = stringPreferencesKey("example_key")

#保存数据

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

读取数据

复制代码
val exampleFlow: Flow<String> = context.dataStore.data
    .map { preferences ->
        // 当键不存在时,返回一个默认值
        preferences[EXAMPLE_KEY] ?: "default_value"
    }

**

七、SharedPreferences & DataStore优劣势

**

DataStore是一个完全超越了SharedPreferences的存在,它的出现就是为了替代SharedPreferences,而它解决的SharedPreferences最大的问题有两点:一是性能问题,二是回调问题。

优势:不丢数据

与MMKV相比,SharedPreferences并没有明显的优势,唯一明显的优势就是不会丢数据。

如果项目中不希望丢数据,并且也不想花时间去做手动的备份和恢复,同时对于MMKV的超高写入性能以及多进程支持都没有需求,那么更应该选择SharedPreferences,而不是MMKV。

但是既然选择了SharedPreferences,那么更应该考虑DataStore,因为前面也说了DataStore在各个方面是完全超越了SharedPreferences的。

卡顿问题

在写方面,SharedPreferences虽然可以用异步的方式来保存更改,以此来避免I/O操作所导致的主线程的耗时;但在Activity启动和关闭的时候,Activity会等待这些异步提交完成保存之后再继续,这就相当于把异步操作转换成同步操作了,从而会导致卡顿甚至ANR(程序未响应)。这是为了保证数据的一致性而不得不做的决定,但它也确实成为了SharedPreferences的一个弱点。而MMKV和DataStore用不同的方式各自都解决了这个问题。

但是SharedPreferences所导致的卡顿和ANR和MMKV的数据损坏一样,都是非常低概率的事件

在读方面,SharedPreferences在读取数据的时候也会卡顿。虽然它的文件加载过程是在后台进行的,但如果代码在它加载完成之前就去尝试读取键值对,线程就会被卡住,直到文件加载完成,而如果这个读取的过程发生在主线程,就会造成界面卡顿,并且数据文件越大就会越卡。这种卡顿,不是SharedPreferences独有的,MMKV也是存在的,因为它初始化的过程同样也是从磁盘里读取文件,而且是一股脑把整个文件读完,所以耗时并不会比SharedPreferences少。而DataStore,就没有这种问题,DataStore不管是读文件还是写文件,都是用的协程在后台进行读写,所有的I/O操作都是在后台线程发生的,所以不论读还是写,都不会卡主线程。

所以对于卡顿问题,SharedPreferences不管读和写都会有卡顿的问题,这个问题MMKV解决了一部分(写时的卡顿),而DataStore完全解决了。

回调问题

DataStore解决的SharedPreferences的另一个问题就是回调。

SharedPreferences如果使用同步方式来保存更改(commit()),会导致主线程的耗时;

但如果使用异步的方式,给它加回调又很不方便,也就是如果你想做一些 异步回调 的工作,会很麻烦。

而DataStore由于是用协程来做的,线程的切换是非常简单的,只需要把回调的内容直接写在保存代码的下方就可以了。

对比来说,MMKV虽然没有使用协程,但是它的速度极快,所以大多数时候并不需要切线程也不会卡顿。

**

八、总结

**

如果有多进程支持的需求,或者有高频写入的需求,应该优先考虑MMKV。

如果没有多进程的需求,也没有高频写入的需求,应该优先被考虑DataStore,因为它在任何时候都不会卡顿,而MMKV在写大字符串和初次加载文件的时候是可能会卡顿的,而且初次加载文件的卡顿不是概率性的,只要文件大到了引起卡顿的程度,就是100%的卡顿。

SP采用的是传统IO+XML的方式实现,效率较低;而MMKV采用的是mmap+protobuf方式实现,序列化后protobuf相对于xml占用体积更小,相应的也带来了更快的传输速度,减小性能消耗。

相关推荐
Ditglu.1 小时前
CentOS7 MySQL5.7 主从复制最终版搭建流程(避坑完整版)
android·adb
2501_941142931 小时前
云原生微服务环境下服务熔断与降级优化实践——提升系统稳定性与容错能力
java·大数据·网络
2501_941404311 小时前
多云环境下微服务化AI大模型的企业部署与优化实践指南
java
恋猫de小郭1 小时前
Android Studio Otter 2 Feature 发布,最值得更新的 Android Studio
android·前端·flutter
浩瀚地学1 小时前
【Java】数组
java·开发语言
走在路上的菜鸟1 小时前
Android学Dart学习笔记第十二节 函数
android·笔记·学习·flutter
没有了遇见1 小时前
Android + Google Play:老项目适配实战指南
android·google
怀君2 小时前
Uniapp——开发Android插件教程
android·uni-app
a***59262 小时前
SpringBoot实现异步调用的方法
java·spring boot·spring