持续创作,加速成长!这是我参与「日新计划 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)
}