持续创作,加速成长!这是我参与「日新计划 6 月更文挑战」的第24天,点击查看活动详情
使用DataStore
替代SharedPreferences
DataStore
是20年Google在JetPack中加入的数据存储的一种解决方案,它提供了一种安全且一致的方式来存储少量数据。并且DataStore
基于协程
和Flow
的方式进行数据的异步存储。并且它的出现直指SharedPreferences
,由于使用协程的方式实现,它的操作是非阻塞式的。DataStore
它提供了两种不同的实现:
- Preferences DataStore:使用键值对的方式进行数据存储
- Proto DataStore:存储类型化的对象数据(使用协议缓冲区来支持)
为什么会替代SharedPreference
DataStore的出现是为了作为SharePreferences的替代方案,为什么Google官方不推荐使用SharePreferences
-
SharedPreference占用内存
Android管理SharedPreference是将加载进内存中的Sp(同一进程中),但是并没有提供一个将Sp对象移除的方法,所以可以知道一旦Sp加载进内存,就会一直常驻内存,直至进程被销毁才会将内存释放
-
SharedPreference的getValue()可能会造成主线程的阻塞
SharedPreference初始化时会将磁盘上的数据加载进内存,如果在加载完成之前去读取数据,那么就会阻塞当前线程。实现如果我们在主线程上进行数据的读取,那么就可能会发生主线程阻塞,界面的卡顿。
-
SharedPreference不能保证类型安全
在使用SharedPreference进行数据存储的时候,我们知道会先将数据存储到HashMap中,子线程会将数据同步写入磁盘,那么我们使用同一个键进行不同类型的数据存储时,就是出现值覆盖的情况,在取的时候我们就会不确定数据类型,而发生ClassCastException
-
apply()
可能会造成主线程阻塞Google官方指出了这个问题主要原因在于apply() 方法是异步的,本身是不会有任何问题,主要是我们前面说到同步数据到磁盘的这个操作,会作为一个任务加入到ActivityThread的一个任务链表中,当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR。
综合上面的原因,所以Google在JetPack中加入DataStore
来替代SharedPreference
,除此之外还有微信团队使用的MMKV的解决方案。今天我们了解下DataStore的使用
preferencesDataStore的使用
preferencesDataStore创建
创建Datastore<Preferences>
的实例,建议使用委托preferencesDataStore
,并指定Preferences DataStore
的名称。
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
一般我们在Kotlin的顶层文件中进行preferencesDataStore
的创建,以便我们在整个应用程序使用
preferencesDataStore中键的定义
DataStore中为我们提供个不同的数据类型,方便我我们快速的构造存储键,我们只需要将键名作为参数传递,但是preferencesDataStore并不会保证类型安全,如果需要保证类型安全可以使用ProtoDataStore
private object PreferencesKeys {
val USER_NAME = stringPreferencesKey("user_name")
val IS_CHECK_REMEBER = booleanPreferencesKey("is_check_remeber")
}
数据的读取
我们可以从dataStore.data
读取一个Flow类型的数据
val userModelFlow: Flow<User> = dataStore.data.map { preferences ->
val userName = preferences[PreferencesKeys.USER_NAME] ?: User.DefaultName
val IsCheck = preferences[PreferencesKeys.IS_CHECK_REMEBER] ?: false
User(userName, IsCheck)
}
我们磁盘读取数据时,流始终会发射一个值,或者会抛出异常。
数据的写入
我们可以使用Preferences DataStore
提供了一个 edit()
函数,它使用事务的方式去更新数据,并且edit接收一个transform代码块,可以根据自己的业务进行值的转换、更新,但是整个代码块都作为单个事务(原子操作)
suspend fun updateUserInfo(user: User) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.USER_NAME] = user.userName
preferences[PreferencesKeys.IS_CHECK_REMEBER] = user.IsCheck
}
}
异常的捕获
DataStore
的主要优势还在于可以进行异常的捕获处理,DataStore在读取/写入数据时发生错误时抛出IOException
。
val userFlow: Flow<User> = dataStore.data.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error reading preferences.", exception)
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
val userName = preferences[PreferencesKeys.USER_NAME] ?: User.DefaultName
val IsCheck = preferences[PreferencesKeys.IS_CHECK_REMEBER] ?: false
User(userName, IsCheck)
}