找回密码
 立即注册
查看: 240|回复: 0

Android Jetpack之DataStore指南

[复制链接]
发表于 2022-6-16 16:51 | 显示全部楼层 |阅读模式
介绍:


在官方尚未出手之前,存储键值对等小型数据集可能普遍采用两种方式,SharedPreferences或是MMKV(如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 Room,而不是 DataStore。DataStore 非常适合简单的小型数据集,不支持部分更新或参照完整性。) MMKV这次暂时不提及,因为DatStore本身对比的也就是SharedPreferences,而且官方也是明确的建议我们迁移到DataStore。 DataStore包含了两种实现方式:
    Preferences DataStore仅使用键存储和访问值数据。此实现不需要预定义的架构,并且不提供类型安全性。Proto DataStore将数据存储为自定义数据类型的实例。此实现要求您使用协议缓冲区(protobuf - PB协议)定义架构,但它提供类型安全性。
与SharedPreferences的对比:


首先我们来看官方的一张对比
功能SharedPreferencesPreferencesDataStoreProtoDataStore
异步 API(仅用于通过监听器读取已更改的值)(通过 Flow 以及 RxJava 2 和 3 Flowable)(通过 Flow 以及 RxJava 2 和 3 Flowable)
同步 API(但无法在界面线程上安全调用)
可在界面线程上安全调用1(这项工作已在后台移至 Dispatchers.IO)(这项工作已在后台移至 Dispatchers.IO)
可以提示错误
不受运行时异常影响2
包含一个具有强一致性保证的事务性 API
处理数据迁移
类型安全 使用协议缓冲区

我们先暂时只看PreferencesDataStoreSharedPreferences 首先同步API和异步API这两点区别是没有问题的。 SharedPreferences:
    apply()来完成异步操作:会立即更改内存中的 SharedPreferences 对象,但会将更新异步写入磁盘。而且apply()还有个问题就是,虽然他本身是异步的来完成IO操作,但是在SharedPreferencesImpl.EditorImpl.apply()中会添加到QueuedWork中,当Service或者Activity启动或停止时,具体可见ActivityThread中handleServiceArgs,handleStopService,handlePauseActivity,handleStopActivity均会执行QueuedWork.waitToFinish()等待数据写入的完成,因为要保证数据不会丢失,但是我们也知道,onPause() 是不适合执行耗时操作的,因为当你期待另一个Activity的时候,会先onPause当前Activity,这很明显,假如你写入了较多内容,然后立马启动了另一个Activity,结果在onPause()被阻塞,就很容易导致ANR。commit()来实现同步操作,但应避免从主线程调用它,因为它可能会阻塞UI线程,这点没什么好说的,而且会返回Boolean值来表示写入是否成功。

详细的可以查看SharedPreferences.Editor接口提供的注释,具体的实现在SharedPreferencesImpl.EditorImpl我这里就不贴源码了。 回到DataStore,PreferencesDataStore本身是基于携程Flow来实现的,所以异步API这点没有任何问题,不过至于同步的使用方式,放到后面来说,我们先看普遍的异步使用方式。我就不一一复述了。
使用:

private const val USER_PREFERENCES_NAME = "user_preferences"private val Context.dataStore by preferencesDataStore(    name = USER_PREFERENCES_NAME)
首先是通过委托拿到DataStore<Preferences>单例.
public fun preferencesDataStore(    name: String,    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,    produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())): ReadOnlyProperty<Context, DataStore<Preferences>> {    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)}internal class PreferenceDataStoreSingletonDelegate internal constructor(    private val name: String,    private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,    private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,    private val scope: CoroutineScope) : ReadOnlyProperty<Context, DataStore<Preferences>> {    private val lock = Any()    @GuardedBy("lock")    @Volatile    private var INSTANCE: DataStore<Preferences>? = null    override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {        return INSTANCE ?: synchronized(lock) {            if (INSTANCE == null) {                val applicationContext = thisRef.applicationContext                INSTANCE = PreferenceDataStoreFactory.create(                    corruptionHandler = corruptionHandler,                    migrations = produceMigrations(applicationContext),                    scope = scope                ) {                    applicationContext.preferencesDataStoreFile(name)                }            }            INSTANCE!!        }    }}
本质上还是走的PreferenceDataStoreFactory.create()创建,是一个非常标准的双重检查锁单例。 来看一下create()函数的参数吧
    corruptionHandler: 异常处理,当反序列化错误时会走到这,可以用于读取错误是返回默认值或捕获异常。migrations: 用于迁移SharedPreferences到PreferenceDataStorescope: 协程的作用域,指定IO操作及数据转换的执行的协程作用域produceFile: 基于提供的Context和name创建或读取对应的文件,默认路径为this.applicationContext.filesDir + datastore/fileName

推荐做法也是通过PreferenceDataStoreFactory来创建DataStore实例并作为单例注入需要它的类中。
读取:

dataStore.data    .catch { exception ->        // 有异常抛出        if (exception is IOException) {        // 使用默认空值            emit(emptyPreferences())        } else {        // 其他异常则继续抛出           throw exception        }    }.map { preferences ->        // 数据转化    }.collect {        // 收集数据    }
emptyPreferences()可以参考上面提到的官方教学示例,里面会详细介绍PreferenceData的KV 可以看出dataStore返回的是一个Flow,你可以很方便的转换成你所需要的数据。
写入:

val INT_KEY = intPreferencesKey("int_key")dataStore.edit { preferences ->    preferences[INT_KEY] = 1}// 调用public suspend fun DataStore<Preferences>.edit(    transform: suspend (MutablePreferences) -> Unit): Preferences {    return this.updateData {        // It's safe to return MutablePreferences since we freeze it in        // PreferencesDataStore.updateData()        it.toMutablePreferences().apply { transform(this) }    }}
直接调用edit函数,不过要注意的是edit可能会抛出异常。 同时还有两点需要注意:
    在transform执行完之前,在transform里改变的值并不会马上更新到DataStore,执行完才会,所以在edit()函数成功返回之前不要认为数据已经写入成功。不要保存在transform中提供的MutablePreferences的引用,在transform外的对preferences操作并不会更新到DataStore,因为这很明显破坏了原设计的原子性与事务性。
总结:


总的来说还是很推荐使用DataStore,与协程的搭配,用起来也是非常的便利,至于PB协议的ProtoDataStore,可以参考官方的示例来实践,差别主要是还是集中在PB文件的处理。

作者:Lowae
链接:https://juejin.cn/post/7109395564789235720
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-26 12:44 , Processed in 0.168823 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表