SharedPreferences 和 DataStore 对比
SharedPreferences:
- 可能阻塞UI线程,导致ANR异常(需要等等sp文件加载完成,而且存储数据越多,文件越大,加载越慢,所有我们之前使用时都会分类存储在不同的sp文件中,如用户信息,业务信息,统计信息等)且不能用于跨进程通信
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
- 加载的数据会一直留在内存中,浪费内存
// 使用静态的ArrayMap缓存每个SP文件,在ContextImpl.getSharedPreferences()中调用
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>()
}
final String packageName = getPackageName()
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName)
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>()
sSharedPrefsCache.put(packageName, packagePrefs)
}
return packagePrefs
}
- 非类型安全的,可能导致ClassCastException异常;
val sp=getSharedPreferences("ljy.sp",Context.MODE_PRIVATE)
sp.edit(commit = true) {
putString("name", "洋仔")
putInt("age", 17)
}
val name=sp.getBoolean("name",false)
val age=sp.getInt("age",false)
- apply() 方法无法获取到操作成功或者失败的结果
DataStore
- 旨在替代原有的 SharedPreferences,支持SharedPreferences数据的迁移
- 基于 Kotlin 协程和 Flow 开发,保证了在主线程的安全性
- 提供两种不同的实现:
- Preferences DataStore:使用键存储和访问数据。
- Proto DataStore: 将数据作为自定义数据类型的实例进行存储。
- 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
- 如果需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 Room,而不是 DataStore。
Preferences DataStore 与 Proto DataStore 区别
- Preferences DataStore 根据键访问xml文件存储的数据,无需事先定义架构,解决了sp的不足;
- Proto DataStore 使用协议缓冲区(protocol buffers)来定义架构,可持久保留强类型数据(可以确保类型安全),与xml存储相比协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了,但需要学习新的序列化机制;
DataStore的使用
使用 Preferences DataStore 存储键值对
添加依赖
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha08"
implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0-rc02")
创建一个常量类
object Constants {
const val MY_SP = "mySP"
const val MY_PREFERENCES = "myPreferences"
const val SP_2_PREFERENCES = "sp2Preferences"
const val KEY_NAME_SP = "name"
val KEY_NAME = stringPreferencesKey(KEY_NAME_SP)
val KEY_USER_NAME = stringPreferencesKey("userName")
val KEY_USER_AGE = intPreferencesKey("userAge")
}
创建 Preferences DataStore
- 在Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 DataStore 保留为单例
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = Constants.MY_PREFERENCES)
数据的读写和清除
dataStore.edit {
it[Constants.KEY_USER_NAME] = "jinYang"
it[Constants.KEY_USER_AGE] = 18
}
dataStore.data.collect {
LjyLogUtil.d("userName:${it[Constants.KEY_USER_NAME]}")
LjyLogUtil.d("userAge:${it[Constants.KEY_USER_AGE]}")
LjyLogUtil.d("$Constants.KEY_NAME:${it[Constants.KEY_NAME]}")
LjyLogUtil.d("it:$it")
}
dataStore.data.asLiveData().observe(this,){
LjyLogUtil.d("asLiveData:userName:${it[Constants.KEY_USER_NAME]}")
LjyLogUtil.d("asLiveData:userAge:${it[Constants.KEY_USER_AGE]}")
LjyLogUtil.d("asLiveData:it:$it")
}
dataStore.edit {
it.clear()
}
迁移 SharedPreferences 到 Preferences DataStore
val Context.dataStore2 by preferencesDataStore(
name = Constants.SP_2_PREFERENCES,
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, Constants.MY_SP))
}
)
dataStore2.data.collect {
LjyLogUtil.d("it:$it")
}
使用 Proto DataStore 存储类型化的对象
- SharedPreferences 和 Preferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。
- Proto DataStore 可利用协议缓冲区定义架构来解决此问题。通过使用协议,DataStore 可以知道存储的类型,并且无需使用键便能提供类型。
添加依赖项
plugins {
...
id "com.google.protobuf" version "0.8.12"
}
implementation("androidx.datastore:datastore:1.0.0-rc02")
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
implementation("androidx.datastore:datastore-rxjava2:1.0.0-rc02")
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
generatedFilesBaseDir = "$projectDir/src/main"
}
android {
sourceSets {
main {
proto {
srcDir 'src/main/proto'
}
}
}
}
定义架构
- Proto DataStore 要求在 app/src/main/proto/ 目录的 proto 文件中保存预定义的架构。此架构用于定义在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅protobuf 语言指南。
- 协议缓冲区是一种对结构化数据进行序列化的机制。只需对数据结构化的方式进行一次定义,编译器便会生成源代码,轻松写入和读取结构化数据。
- 在 app/src/main/proto 目录中创建一个名为 user_prefs.proto 的新文件,其内容如下
syntax = "proto3"
//包名
option java_package = "com.jinyang.jetpackdemo.datastore"
option java_multiple_files = true
option java_outer_classname = "UserInfoProto"
message User {
//格式:字段类型 + 字段名称 + 字段编号
string name = 1
int32 age = 2
bool isMarried = 3
}
- 创建完成后,Rebuild Project,即可看到app/src/main/debug下自动生成的文件
创建序列化器
object UserSerializer:Serializer<User>{
override val defaultValue: User
get() = User.getDefaultInstance()
override suspend fun readFrom(input: InputStream): User {
return User.parseFrom(input)
}
override suspend fun writeTo(t: User, output: OutputStream) {
t.writeTo(output)
}
}
创建DataStore
val Context.userInfoStore: DataStore<User> by dataStore(
fileName = "userInfo.pb",
serializer = UserSerializer
)
读写内容及在同步异步代码中的使用
- 请尽可能避免在 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 ANR 或界面卡顿,而阻塞其他线程可能会导致死锁;
private fun dataStoreProto() {
userInfoStore.data.asLiveData().observe(this) {
LjyLogUtil.d("asLiveData:it:$it")
LjyLogUtil.d("name:${it.name}")
LjyLogUtil.d("age:${it.age}")
LjyLogUtil.d("isMarried:${it.isMarried}")
}
lifecycleScope.launch {
userInfoStore.updateData {
it.toBuilder()
.setName("今阳")
.setAge(18)
.setIsMarried(true)
.build()
}
userInfoStore.data.collect {
LjyLogUtil.d("collect: it:$it")
}
}
val user = runBlocking { userInfoStore.data.first() }
LjyLogUtil.d("runBlocking: user:$user")
lifecycleScope.launch {
val user = userInfoStore.data.first()
LjyLogUtil.d("user:$user")
}
}
迁移 SharedPreferences 到 Proto DataStore
val Context.userInfoStore2: DataStore<User> by dataStore(
fileName = "userInfo2.pb",
serializer = UserSerializer,
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, Constants.MY_SP) { sharedPrefs, user ->
user.toBuilder()
.setName(sharedPrefs.getString(Constants.KEY_NAME_SP))
.setAge(sharedPrefs.getInt(Constants.KEY_NAME_SP,0))
.setIsMarried(false)
.build()
})
}
)
lifecycleScope.launch {
val user = userInfoStore2.data.first()
LjyLogUtil.d("user:$user")
}
MMKV
- 替换sp还有另外一种比较不错的选择,就是腾讯开源的MMKV;
- MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。
使用流程
1. 添加依赖
implementation 'com.tencent:mmkv-static:1.2.10'
2. 初始化,在Application.onCreate()中
val rootDir = MMKV.initialize(this)
LjyLogUtil.d(rootDir)
3. CRUD
val kv = MMKV.defaultMMKV()
kv.encode("name", "LJY")
kv.encode("age", 16)
kv.encode("isMarried", true)
val name = kv.decodeString("name")
LjyLogUtil.d("name=$name")
val age = kv.decodeInt("age")
LjyLogUtil.d("age=$age")
val isMarried = kv.decodeBool("isMarried")
LjyLogUtil.d("isMarried=$isMarried")
kv.removeValueForKey("age");
LjyLogUtil.d("age=${kv.decodeInt("age")}")
4. 如果不同业务需要区别存储,也可以单独创建自己的实例
val kvUser = MMKV.mmkvWithID("userInfo")
kvUser.encode("name", "yang")
LjyLogUtil.d("name=${kvUser.decodeString("name")}")
5. 如果业务需要多进程访问,那么在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODE
val kvSetting = MMKV.mmkvWithID("settings", MMKV.MULTI_PROCESS_MODE)
kvSetting.encode("key", "abc")
LjyLogUtil.d("key=${kvSetting.decodeString("key")}")
6. 迁移 SharedPreferences 到 MMKV
- MMKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface,
在迁移的时候只需两三行代码即可,其他 CRUD 操作代码都不用改
val sp2mmkv: MMKV = MMKV.mmkvWithID("myData")
val sp = getSharedPreferences(Constants.MY_SP, MODE_PRIVATE)
sp2mmkv.importFromSharedPreferences(sp)
sp.edit().clear().apply()
sp2mmkv.edit(commit = true){
putBoolean("bool", true)
val set = HashSet<String>()
set.add("a")
set.add("b")
set.add("c")
putStringSet("string-set", set)
}
LjyLogUtil.d("name=${sp2mmkv.getString(Constants.KEY_NAME_SP,"")}")
LjyLogUtil.d("age=${sp2mmkv.getInt(Constants.KEY_AGE_SP,0)}")
LjyLogUtil.d("bool=${sp2mmkv.getBoolean("bool",false)}")
LjyLogUtil.d("string-set=${sp2mmkv.getStringSet("string-set", emptySet())}")
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章