美文网首页Android Jetpack 系列
Android Jetpack系列--8. DataStore使

Android Jetpack系列--8. DataStore使

作者: 今阳说 | 来源:发表于2021-09-03 09:31 被阅读0次

    SharedPreferences 和 DataStore 对比

    SharedPreferences:

    1. 可能阻塞UI线程,导致ANR异常(需要等等sp文件加载完成,而且存储数据越多,文件越大,加载越慢,所有我们之前使用时都会分类存储在不同的sp文件中,如用户信息,业务信息,统计信息等)且不能用于跨进程通信
    // ContextImpl.getSharedPreferences()
    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;
            }
        }
        //MODE_MULTI_PROCESS只是重新加载一下sp,并不能保证跨进程安全的
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it.  This has been the
            // historical (if undocumented) behavior.
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }
    //1. SharedPreferences对象的创建会开启异步线程读取数据
    //SharedPreferencesImpl.startLoadFromDisk()
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    
    //2. SharedPreferences.getXX()是同步的,会调用wait方法等待对象加载完毕,就可能导致ANR
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    //3. SharedPreferences.apply()是异步的,但是当生命周期处于handleStopService(),handlePauseActivity(),
    //handleStopActivity() 时, 会一直等待apply()方法将数据保存成功,否则会一直等待,就可能导致ANR;
    
    1. 加载的数据会一直留在内存中,浪费内存
    // 使用静态的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;
    }
    
    1. 非类型安全的,可能导致ClassCastException异常;
    val sp=getSharedPreferences("ljy.sp",Context.MODE_PRIVATE)
    //sp.edit默认是apply,可以手动设置为commit
    //apply:提交会先写入内存,然后异步写入磁盘
    //commit:直接写入磁盘
    //如果频繁操作的话 apply 的性能会优于 commit
    //提交数据时, 尽量使用apply,而非commit,仅当需要确定提交结果,并据此有后续操作时,使用commit;
    sp.edit(commit = true) {
        putString("name", "洋仔")
        putInt("age", 17)
    }
    val name=sp.getBoolean("name",false)
    val age=sp.getInt("age",false)
    
    1. apply() 方法无法获取到操作成功或者失败的结果

    DataStore

    • 旨在替代原有的 SharedPreferences,支持SharedPreferences数据的迁移
    • 基于 Kotlin 协程和 Flow 开发,保证了在主线程的安全性
    • 提供两种不同的实现:
      • Preferences DataStore:使用键存储和访问数据。
      • Proto DataStore: 将数据作为自定义数据类型的实例进行存储。
    • 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
    • 如果需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 Room,而不是 DataStore。
    Preferences DataStore 与 Proto DataStore 区别
    1. Preferences DataStore 根据键访问xml文件存储的数据,无需事先定义架构,解决了sp的不足;
    2. Proto DataStore 使用协议缓冲区(protocol buffers)来定义架构,可持久保留强类型数据(可以确保类型安全),与xml存储相比协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了,但需要学习新的序列化机制;

    DataStore的使用

    使用 Preferences DataStore 存储键值对

    添加依赖
    //(1) Datastore Preferences
    implementation "androidx.datastore:datastore-preferences:1.0.0-alpha08"
    // optional - RxJava2 support
    implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0-rc02")
    
    创建一个常量类
    • 便于管理文件名及键值对的key
    object Constants {
        //SharedPreferences文件名
        const val MY_SP = "mySP"
    
        //DataStore<Preferences>文件名
        const val MY_PREFERENCES = "myPreferences"
    
        //SharedPreferences 迁移到 DataStore<Preferences> 后的文件名
        const val SP_2_PREFERENCES = "sp2Preferences"
    
        //SharedPreferences中的key
        const val KEY_NAME_SP = "name"
    
        //SharedPreferences 迁移到 DataStore<Preferences> 后的key
        val KEY_NAME = stringPreferencesKey(KEY_NAME_SP)
    
        //DataStore<Preferences> 中的key
        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)
    
    数据的读写和清除
    //1. 存储键值对数据
    dataStore.edit {
        it[Constants.KEY_USER_NAME] = "jinYang"
        it[Constants.KEY_USER_AGE] = 18
    }
    
    //2. 读取键值对数据
    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")
    }
    //或者使用LiveData
    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")
    }
    
    //3. 清除数据
    dataStore.edit {
        it.clear()
    }
    
    
    迁移 SharedPreferences 到 Preferences DataStore
    //1. 构建DataStore时,produceMigrations参数传入一个SharedPreferencesMigration的集合,
    // 即可把多个sp文件关联到DataStore
    val Context.dataStore2 by preferencesDataStore(
        name = Constants.SP_2_PREFERENCES,
        produceMigrations = { context ->
            // Since we're migrating from SharedPreferences, add a migration based on the
            // SharedPreferences name
            listOf(SharedPreferencesMigration(context, Constants.MY_SP))
        }
    )
    
    //2. 需要执行一次读/写操作才能完成迁移,迁移成功后会自动删除sp文件,
    // 需要注意的是迁移工作只执行一次,迁移成功后需要停止再使用sp
    dataStore2.data.collect {
        LjyLogUtil.d("it:$it")
    }
    

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

    • SharedPreferences 和 Preferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。
    • Proto DataStore 可利用协议缓冲区定义架构来解决此问题。通过使用协议,DataStore 可以知道存储的类型,并且无需使用键便能提供类型。
    添加依赖项
    //1. 添加协议缓冲区插件
    plugins {
        ...
        id "com.google.protobuf" version "0.8.12"
    }
    
    //2 添加协议缓冲区和 Proto DataStore 依赖项
    implementation("androidx.datastore:datastore:1.0.0-rc02")
    //protobuf
    implementation  "com.google.protobuf:protobuf-javalite:3.10.0"
    // optional - RxJava2 support
    implementation("androidx.datastore:datastore-rxjava2:1.0.0-rc02")
    
    //3. 配置协议缓冲区
    protobuf {
        // 设置 protoc 的版本
        protoc {
            // //从仓库下载 protoc 这里的版本号需要与依赖 com.google.protobuf:protobuf-javalite:xxx 版本相同
            artifact = 'com.google.protobuf:protoc:3.10.0'
        }
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    java {
                        option "lite"
                    }
                }
            }
        }
    
        // 默认生成目录 $buildDir/generated/source/proto 通过 generatedFilesBaseDir 改变生成位置
        generatedFilesBaseDir = "$projectDir/src/main"
    }
    
    //4. 设置 proto 文件位置
    android {
        sourceSets {
            main {
                proto {
                    // proto 文件默认路径是 src/main/proto
                    // 可以通过 srcDir 修改 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下自动生成的文件
    创建序列化器
    • 自定义Serializer
    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 {
            //将内容写入 Proto DataStore
            userInfoStore.updateData {
                it.toBuilder()
                    .setName("今阳")
                    .setAge(18)
                    .setIsMarried(true)
                    .build()
            }
            //使用collect读取内容
            userInfoStore.data.collect {
                LjyLogUtil.d("collect: it:$it")
            }
        }
        // DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。
        // 如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。
        // Kotlin 协程提供 runBlocking() 协程构建器,以帮助消除同步与异步代码之间的差异。
        // 您可以使用 runBlocking() 从 DataStore 同步读取数据。RxJava 在 Flowable 上提供阻塞方法。
        // 以下代码会阻塞发起调用的线程,直到 DataStore 返回数据:
        val user = runBlocking { userInfoStore.data.first() }
        LjyLogUtil.d("runBlocking: user:$user")
        //对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:
        lifecycleScope.launch {
            val user = userInfoStore.data.first()
            LjyLogUtil.d("user:$user")
        }
    }
    
    迁移 SharedPreferences 到 Proto DataStore
    //1. 创建DataStore,并将sp的键值对映射到User
    val Context.userInfoStore2: DataStore<User> by dataStore(
        fileName = "userInfo2.pb",
        serializer = UserSerializer,
        produceMigrations = { context ->
            listOf(SharedPreferencesMigration(context, Constants.MY_SP) { sharedPrefs, user ->
                //将sp的键值对映射到User
                user.toBuilder()
                    .setName(sharedPrefs.getString(Constants.KEY_NAME_SP))
                    .setAge(sharedPrefs.getInt(Constants.KEY_NAME_SP,0))
                    .setIsMarried(false)
                    .build()
            })
        }
    )
    //2. 执行一次读写
    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)
    //默认为:/data/user/0/com.jinyang.jetpackdemo/files/mmkv
    
    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 preferences = getSharedPreferences("myData", MODE_PRIVATE)
    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())}")
    

    我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

    相关文章

      网友评论

        本文标题:Android Jetpack系列--8. DataStore使

        本文链接:https://www.haomeiwen.com/subject/gwghwltx.html