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

发布于:2025-02-10 ⋅ 阅读:(52) ⋅ 点赞:(0)

简介:本文介绍了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忍一忍吧。

总结对比:

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

结论:

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


网站公告

今日签到

点亮在社区的每一天
去签到