• 【Android Jetpack】DataStore的介绍


    DataStore

    Jetpack DataStore是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore使用Kotlin协程和Flow以异步、一致的事务方式存储数据。

    注意:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 Room,而不是 DataStore。DataStore非常适合简单的小型数据集,不支持部分更新或参照完整性。

    Preferences DataStore和Proto DataStore

    DataStore提供两种不同的实现:Preferences DataStoreProto DataStore

    • Preferences DataStore像SharedPreferences一样,以键值对的形式进行基本类型的数据存储。DataStore基于Flow实现异步存储,避免因为阻塞主线程带来的ANR问题
    • Proto DataStore基于Protobuf实现任意自定义类型的数据存储,需要定义Protobuf的IDL,但是可以保证类型安全的访问。存储类的对象(typed objects ),通过protocol buffers将对象序列化存储在本地,protocol buffers现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用。

    SharedPreferences的缺点

    SharedPreference是一个轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,初始化SharedPreference的时候,会将整个文件内容加载内存中,因此会带来以下问题:

    • 通过getXXX()方法获取数据,可能会导致主线程阻塞

      所有getXXX()方法都是同步的,在主线程调用get方法,必须等待SharedPreference加载完毕,会导致主线程堵塞。

    • SharedPreference不能保证类型安全

      调用getXXX()方法的时候,可能会出现ClassCastException异常,因为使用相同的key进行操作的时候,putXXX()方法可以使用不同类型的数据覆盖相同的key。

    • SharedPreference 加载的数据会一直留在内存中,浪费内存

      通过getSharedPreferences()方法加载的数据,最后会将数据存储在静态的成员变量中。

    • apply()方法虽然是异步的,但是可能会发生ANR,在8.0之前和8.0之后实现各不相同
      apply()方法是异步的,本身是不会有任何问题,但是当生命周期处于handleStopService()handlePauseActivity()handleStopActivity()的时候会一直等待apply()方法将数据保存成功,否则会一直等待,从而阻塞主线程造成ANR。

    • apply()方法无法获取到操作成功或者失败的结果

      apply()方法无法获取到操作成功或者失败的结果,而commit()方法是可以接收MemoryCommitResult里面的一个boolean参数作为结果

    • SP不能用于跨进程通信

    DataStore的优点

    Preferences DataStore主要用来替换SharedPreferences,Preferences DataStore解决了SharedPreferences带来的所有问题

    **Preferences DataStore相比于SharedPreferences优点: **

    • DataStore是基于Flow实现的,所以保证了在主线程的安全性
    • 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
    • 自动完成SharedPreferences迁移到DataStore,保证数据一致性,不会造成数据损坏
    • 可以监听到操作成功或者失败结果

    使用Preferences DataStore存储键值对

    Preferences DataStore实现使用DataStore和Preferences 类将简单的键值对保留在磁盘上。

    创建Preferences DataStore

    使用由preferencesDataStore 创建的属性委托来创建Datastore实例。在您的Kotlin文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将DataStore保留为单例。

    // At the top level of your kotlin file
    val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") //name = "" 中填写的为名字
    
    • 1
    • 2

    从Preferences DataStore读取内容

    由于Preferences DataStore不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在DataStore实例中的每个值定义一个键

    例如,如需为int 值定义一个键,请使用intPreferencesKey()。然后,使用DataStore.data 属性,通过Flow 提供适当的存储值。

    val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
    val exampleCounterFlow: Flow<Int> = context.dataStore.data .map {
        preferences -> 
        // No type safety.
        preferences[EXAMPLE_COUNTER] ?: 0}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    将内容写入Preferences DataStore

    Preferences DataStore提供了一个edit() 函数,用于以事务方式更新DataStore中的数据。该函数的transform参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。

    suspend fun incrementCounter() { context.dataStore.edit { settings ->  val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0  settings[EXAMPLE_COUNTER] = currentCounterValue + 1 }}
    
    • 1

    使用Proto DataStore存储类型化的对象

    既生Preference DataStore何生Proto DataStore,它们之间有什么区别?

    • Preference DataStore主要是为了解决SharedPreferences所带来的性能问题
    • Proto DataStore比Preference DataStore 更加灵活,支持更多的类型
      • Preference DataStore支持Int、Long、Boolean、Float、String
      • protocol buffers支持的类型,Proto DataStore都支持
    • Preference DataStore以XML的形式存储key-value数据,可读性很好
    • Proto DataStore使用了二进制编码压缩,体积更小,速度比XML更快

    序列化

    序列化:将一个对象转换成可存储或可传输的状态,数据可能存储在本地或者在蓝牙、网络间进行传输。序列化大概分为对象序列化、数据序列化。

    对象的序列化

    Java对象序列化 将一个存储在内存中的对象转化为可传输的字节序列,便于在蓝牙、网络间进行传输或者存储在本地。把字节序列还原为存储在内存中的Java对象的过程称为反序列化

    在Android中可以通过Serializable和Parcelable两种方式实现对象序列化。

    Serializable

    Serializable是Java原生序列化的方式,主要通过ObjectInputStream和ObjectOutputStream来实现对象序列化和反序列化,但是在整个过程中用到了大量的反射和临时变量,会频繁的触发GC,序列化的性能会非常差,但是实现方式非常简单,来看一下ObjectInputStream和ObjectOutputStream源码里有很多反射的地方。

    ObjectOutputStream.java
    private void writeObject0(Object obj, boolean unshared)
            throws IOException{
            ......
            Class<?> cl = obj.getClass();
            ......
    }
    
    ObjectInputStream.java
    void readFields() throws IOException {
        ......
        ObjectStreamField[] fields = desc.getFields(false);
        for (int i = 0; i < objVals.length; i++) {
            objVals[i] =
                readObject0(fields[numPrimFields + i].isUnshared());
            objHandles[i] = passHandle;
        }
        ......
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在Android中存在大量跨进程通信,由于Serializable性能差的原因,所以Android需要更加轻量且高效的对象序列化和反序列化机制,因此Parcelable出现了。

    Parcelable

    Parcelable的出现解决了Android中跨进程通信性能差的问题,而且Parcelable比Serializable要快很多,因为写入和读取的时候都是采用自定义序列化存储的方式,通过writeToParcel()方法和describeContents()方法来实现,不需要使用反射来推断它,因此性能得到提升,但是使用起来比Serializable要复杂很多。

    Kotlin语言的话可以使用 @Parcelize注解,快速的实现Parcelable序列化。

    Serializable和Parcelable的区别:

    img

    数据序列化

    对象序列化记录了很多信息,包括Class信息、继承关系信息、变量信息等等,但是数据序列化相比于对象序列化就没有这么多沉余信息,数据序列化常用的方式有JSON、Protocol Buffers、FlatBuffers。

    • JSON:是一种轻量级的数据交互格式,支持跨平台、跨语言,被广泛用在网络间传输,JSON的可读性很强,但是序列化和反序列化性能却是最差的,解析过程中,要产生大量的临时变量,会频繁的触发GC,为了保证可读性,并没有进行二进制压缩,当数据量很大的时候,性能上会差一点。

    • Protocol Buffers:它是Google开源的跨语言编码协议,可以应用到C++、C#、Dart 、Go 、Java、Python等等语言,Google内部几乎所有RPC都在使用这个协议,使用了二进制编码压缩,体积更小,速度比JSON更快,但是缺点是牺牲了可读性

      RPC指的是跨进程远程调用,即一个进程调用另外一个进程的方法。

    • FlatBuffers:同Protocol Buffers一样是Google开源的跨平台数据序列化库,可以应用到C++、C#,Go、Java、JavaScript、PHP、Python 等等语言,空间和时间复杂度上比其他的方式都要好,在使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小,但是缺点是牺牲了可读性

    最后我们用一张图来分析一下JSON、Protocol Buffers、FlatBuffers 它们序列化和反序列的性能,数据来源于JSON vs Protocol Buffers vs FlatBuffers

    img

    FlatBuffers和Protocol Buffers无论是序列化还是反序列都完胜JSON,FlatBuffers 最初是Google为游戏或者其他对性能要求很高的应用开发的,接下来我们来看一下今天主角Protocol Buffer。

    Protocol Buffer

    Protocol Buffer(简称Protobuf) 它是Google开源的跨语言编码协议,可以应用到C++、C#、Dart、Go、Java、Python等等语言,Google内部几乎所有RPC都在使用这个协议,使用了二进制编码压缩,体积更小,速度比JSON更快。

    Proto3.0.0 Release Note 得知:protocol buffers最初开源时,它实现了Protocol Buffers语言版本2(称为 proto2), 这也是为什么版本数从v2.0.0开始,从v3.0.0开始, 引入新的语言版本(proto3),而旧的版本(proto2)继续被支持。所以到目前为止Protobuf共两个版本proto2和proto3。

    proto2和proto3应该学习那个版本?

    proto3简化了proto2的语法,提高了开发的效率,因此也带来了版本不兼容的问题,因为2019年的时候才发布proto3稳定版本,所以在这之前使用Protocol Buffer的公司,大部分项目都是使用proto2的版本,从上文的源码分析部分可知,在DataStore中使用了proto2语法,所以proto2和proto3这两种语法都同时在使用。

    对于初学者而言直接学习proto3语法就可以了,为了适应技术迭代的变化,当掌握proto3语法之后,可以顺带了解一下proto2语法以及proto3和proto2语法的区别,这样可以更好的理解其他的开源项目。

    为了避免混淆proto3和proto2语法,在本文仅仅分析proto3语法,当我们了解完这些基本概念之后,我们开始分析如何在项目中使用Proto DataStore。

    Proto DataStore实现使用DataStore和协议缓冲区将类型化的对象保留在磁盘上。

    定义架构

    Proto DataStore 要求在 app/src/main/proto/ 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 protobuf 语言指南

    syntax = "proto3"; option java_package = "com.example.application"; option java_multiple_files = true; message Settings {  int32 example_counter = 1; } 
    
    • 1

    注意:您的存储对象的类在编译时由 proto 文件中定义的 message 生成。请务必重新构建您的项目。

    创建 Proto DataStore

    创建 Proto DataStore 来存储类型化对象涉及两个步骤:

    1. 定义一个实现 Serializer 的类,其中 T 是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。
    2. 使用由 dataStore 创建的属性委托来创建 DataStore 的实例,其中 T 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore 第 1 步中定义的序列化器类的名称。

    KotlinJava

    object SettingsSerializer : Serializer { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings {  try {   return Settings.parseFrom(input)  } catch (exception: InvalidProtocolBufferException) {   throw CorruptionException("Cannot read proto.", exception)  } } override suspend fun writeTo(  t: Settings,  output: OutputStream) = t.writeTo(output)}val Context.settingsDataStore: DataStore by dataStore( fileName = "settings.pb", serializer = SettingsSerializer)
    
    • 1

    从 Proto DataStore 读取内容

    使用 DataStore.data 显示所存储对象中相应属性的 Flow

    KotlinJava

    val exampleCounterFlow: Flow = context.settingsDataStore.data .map { settings ->  // The exampleCounter property is generated from the proto schema.  settings.exampleCounter }
    
    • 1

    将内容写入 Proto DataStore

    Proto DataStore 提供了一个 updateData() 函数,用于以事务方式更新存储的对象。updateData() 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。

    KotlinJava

    suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings ->  currentSettings.toBuilder()   .setExampleCounter(currentSettings.exampleCounter + 1)   .build()  }}
    
    • 1

    在同步代码中使用 DataStore

    注意:请尽可能避免在 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 ANR 或界面卡顿,而阻塞其他线程可能会导致死锁

    DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。

    Kotlin 协程提供 runBlocking() 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 runBlocking() 从 DataStore 同步读取数据。RxJava 在 Flowable 上提供阻塞方法。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据:

    KotlinJava

    val exampleData = runBlocking { context.dataStore.data.first() }
    
    • 1

    对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:

    KotlinJava

    override fun onCreate(savedInstanceState: Bundle?) {  lifecycleScope.launch {    context.dataStore.data.first()    // You should also handle IOExceptions here.  }}
    
    • 1

    这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 runBlocking() 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。

    https://developer.android.com/topic/libraries/architecture/datastore#typed-datastore

    https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html

    https://blog.csdn.net/zzw0221/article/details/109274610

    SharedPreferences 有着许多缺陷: 看起来可以在 UI 线程安全调用的同步 API 其实并不安全、没有提示错误的机制、缺少事务 API 等等。DataStore 是 SharedPreferences 的替代方案,它解决了 Shared Preferences 的绝大部分问题。DataStore 包含使用 Kotlin 协程和 Flow 实现的完全异步 API,可以处理数据迁移、保证数据一致性,并且可以处理数据损坏。

    https://blog.csdn.net/weixin_42324979/article/details/112650189?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control

    缺点:

    • 目前Jetpack Security没有支持DataStore,所以不能像SharedPreference一样支持加密
    • 不能安全的进行IPC,这点相对于SharedPreferences没有提升,有较强IPC需求的话首选MMKV
    • 使用PB进行序列化时需要额外定义IDL,这会产生一定工作量

    最后用一张表格来对比一下 MMKV、DataStore、SharedPreferences 的不同之处,如果发现错误,或者有其他不同之处,期待你来一起完善。

    img

    目前为止,MMKV 功能比 DataStore 强大,毕竟 MMKV 已经经历好几个年头了, DataStore 才只是 alpha01 版本,但是 DataStore 作为 Jetpack 的成员,背靠 Google 的支持,加上全球的开发者一起来完善,从未来角度 DataStore 功能会逐渐完善。DataStore 的版本还是挺优势的,例如:类型安全检查,DataStore 编译的时候,就会告诉开发者类型不对,相比于运行时错误,编译时错误提示更好

  • 相关阅读:
    linux中关机和重启命令:shutdown
    Java版本spring cloud + spring boot企业电子招投标系统源代码
    公钥加密如何确保数据的完整性
    软件设计包括了四个既独立又相互联系的活动
    【Linux】Linux环境搭建
    指针笔试题~走近大厂
    路由进阶--编程式导航(在Vue路由中实现跳转,跳转传参)
    京东数据分析软件:2023年8月京东护肤行业品牌销售排行榜
    【HCIA】OSI 模型
    Centos 7.6 安装部署 openGauss 3.1.0 企业版一主两备集群
  • 原文地址:https://blog.csdn.net/weixin_73871834/article/details/134498107