简介:本文介绍了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文件内容。
@Overridepublic 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.MMKVclass 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.mapprivate 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 协程和流。