一、背景
键值对的存储在android开发功能时非常常见。如模式的开/关、系统状态、界面状态等,这些很适合用键值对来存。
键值对的存储方案,最常用的就是Android自带的 SharedPreferences。SharedPreferences使用起来很简单,但是却有一个致命的问题--卡顿,甚至有时候会出现ANR。
内部存储框架存在的问题:
- SP存在跨进程不安全,数据量⼤的时候加载缓慢,全量写⼊可能引起ANR问题
- 读取SP内容时,由于读取操作是同步的,必须等待SP加载完毕后才能读取,也有可能导致ANR
在top10的ANR中,SP导致的问题占40%
二、SharedPreferences的优劣势:
SharedPreferences是Android平台上用于存储简单数据的轻量级机制。它主要用于保存应用中的偏好设置和配置信息。
优势:
-
简单易用:
- SharedPreferences提供了简单的API,用于存储和检索基本类型的数据(如布尔值、浮点数、整型、长整型和字符串)。
- 适合存储少量的键值对数据,使用起来非常方便。
-
持久性:
- 数据存储在XML文件中,应用关闭后数据依然存在,不会丢失。
- 自动处理数据的持久化,开发者无需额外操作。
-
访问速度快:
- 由于数据量小,读取和写入操作速度较快。
- 适合存储需要频繁访问的小型数据。
-
适合存储用户设置:
- 特别适合保存用户偏好设置,如应用主题、语言选择等。
劣势:
-
不适合存储大数据:
- SharedPreferences主要用于存储小型数据,不适合存储复杂或大型数据结构。
- XML文件结构限制了其存储能力和性能。
-
数据结构单一:
- 只能存储简单的键值对,无法直接存储对象或列表。
- 对于复杂数据结构,需要进行序列化处理。
-
线程安全问题:
- SharedPreferences本身是线程安全的,但在多线程环境下进行写操作时,可能需要额外的同步处理。
-
加密支持有限:
- 数据以明文形式存储,敏感信息需要额外处理(如加密)以确保安全。
-
性能问题:
- 在频繁写入大量数据时可能导致性能下降,因为每次写入都会修改底层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占用体积更小,相应的也带来了更快的传输速度,减小性能消耗。