Android使用DataStore保存数据之后断电重启设备数据丢失临时解决办法

前言:

DataStore 被推荐用来取代 SharedPreferences 后,我便将其应用于项目之中。然而在实际使用过程中,却遭遇了严重的问题:一旦发生立即断电重启的情况,数据不仅无法保存,甚至还会出现损坏且无法恢复的状况!这简直如同一场灾难。

通过 Google IssueTracker 进行查询后得知,这个问题自被发现之初至今,已然过去两年有余。从 DataStore1.0.0 版本直至 1.1.0 版本,该问题始终未得到解决,而且官网文档也未公布下一个版本的发布时间,这似乎意味着在短期内此问题都难以得到修复。不过,不得不说 DataStoreFlow 和协程配合使用时,所展现出的便利性是极具吸引力的,因此在当前阶段,我也不太愿意对其进行大规模的改动。


问题描述

在项目里,我只是采用了 DataStore 简单的 <Key,Value> 模式来存储用户首选项数据,本想着这样能方便又高效地完成数据存储任务。可谁能料到,在遇到立即断电这种情况时,却出现了严重的问题。不但新的数据没办法存储下来,更糟糕的是,还会致使其他原本正常的数据一并遭到损坏,并且这些损坏的数据根本没办法恢复,实在是让人头疼不已呀。

而且呢,下面相关的使用方法都是原原本本照着官网来操作的,按道理来说不应该出现问题才对,可偏偏就在立即断电重启这样的场景下,还是出现了故障,这着实让人有些无奈和困扰啊。


val Context.userSettingsDataStore by preferencesDataStore("user_settings")

data class UserSettings(val isDark: Boolean)

class UserSettingsRepository(context: Context) {


    private val dataStore = context.userSettingsDataStore

    private object PreferencesKeys {

        val KEY_DARK = booleanPreferencesKey("is_dark")
    }

    val userSettingsFlow = dataStore.data.catch { ex ->
        if (ex is IOException) {
            emit(emptyPreferences())
        } else {
            throw ex
        }
    }.map {
        it[PreferencesKeys.KEY_DARK] ?: false
    }

    suspend fun setThemeDark(dark: Boolean) {
        dataStore.edit {
            it[PreferencesKeys.KEY_DARK] = dark
        }
    }
}

原因分析:

目前对于出现这种问题的原因,我暂时还没能想明白呀。总感觉导致这个问题出现的因素不止一处,可能涉及到多个方面的情况交织在一起了。而且我也向官方反馈了这个情况,可到现在官方都还没有给出任何回复呢,就只能这么干等着,心里实在没底,也不知道什么时候才能把这个棘手的问题给解决掉啊。


解决(临时解决)方案:

经过一番测试后发现,在面对立即断电重启这样的情况时,SharedPreferences 的表现相当稳定,数据既不会丢失,更不会出现损坏的情况。基于这个测试结果,我琢磨出了一个思路,那就是在使用 DataStore 进行数据存储的同时,也另外存储一份相同的数据到 SharedPreferences 当中。如此一来,等到下次启动应用的时候,就可以先从 SharedPreferences 里读取数据,然后再把这些数据重新写入到 DataStore 里面去。
可能有人会问了,既然都已经回过头去用 SharedPreferences 了,那干嘛还非要执着于使用 DataStore 呢?其实啊,重点就在于 DataStore 配合 Flow 来对流式监听数据变化这一功能真的是太好用了,仅凭这一点,就让我对 DataStore 依旧抱有一丝希望,盼着官方能够尽快修复它存在的这个问题呀。
以下就是我目前想到的临时解决办法:
我新建了一个名为 DataStoreBackup 的类,用它来替换掉原来 DataStoreeditupdateData 方法。在创建 DataStore 单例的时候呢,会从 SharedPreferences 中重新读取数据,通过这样的方式来尽量保证数据的完整性以及应用在应对断电重启等情况时的稳定性,虽然只是个临时举措,但也算是目前能想到的比较可行的办法了。


import android.content.Context
import androidx.annotation.GuardedBy
import androidx.datastore.core.DataMigration
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.mutablePreferencesOf
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

fun preferencesDataStoreAndBackup(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? =
        ReplaceFileCorruptionHandler {
            it.printStackTrace()
            emptyPreferences()
        },
    produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStoreBackup> {
    return PreferenceDataStoreBackupSingletonDelegate(
        name,
        corruptionHandler,
        produceMigrations,
        scope
    )
}


class DataStoreBackup(
    context: Context,
    name: String,
    private val dataStore: DataStore<Preferences>
) {

    private val sp by lazy {
        context.getSharedPreferences(name, Context.MODE_PRIVATE)
    }

    val data get() = dataStore.data


    suspend fun edit(
        transform: suspend (MutablePreferences) -> Unit
    ) {
        this.updateData(transform)
    }

    suspend fun updateData(transform: suspend (MutablePreferences) -> Unit) {
        dataStore.updateData {

            editBackup(transform)

            it.toMutablePreferences().apply {
                transform.invoke(this)
            }
        }
    }

    private suspend fun editBackup(transform: suspend (MutablePreferences) -> Unit) {
        val newData = mutablePreferencesOf()
        transform.invoke(newData)
        withContext(Dispatchers.IO) {
            val editor = sp.edit()
            newData.asMap().keys.forEach {
                val key = it.name
                when (val value = newData[it]) {
                    is Boolean -> {
                        editor.putBoolean(key, value)
                    }

                    is Long -> {
                        editor.putLong(key, value)
                    }

                    is Int -> {
                        editor.putInt(key, value)
                    }

                    is Float -> {
                        editor.putFloat(key, value)
                    }

                    is String -> {
                        editor.putString(key, value)
                    }

                    is Set<*> -> {
                        @Suppress("UNCHECKED_CAST")
                        editor.putStringSet(key, value as? Set<String> ?: emptySet())
                    }
                }
            }
            editor.commit()
        }
    }

}


internal class PreferenceDataStoreBackupSingletonDelegate internal constructor(
    private val name: String,
    private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,
    private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStoreBackup> {

    private val lock = Any()

    @GuardedBy("lock")
    @Volatile
    private var INSTANCE: DataStoreBackup? = null

    /**
     * Gets the instance of the DataStore.
     *
     * @param thisRef must be an instance of [Context]
     * @param property not used
     */
    override fun getValue(thisRef: Context, property: KProperty<*>): DataStoreBackup {
        return INSTANCE ?: synchronized(lock) {
            if (INSTANCE == null) {
                val applicationContext = thisRef.applicationContext
                val backupFileName = name + "_backup"
                val dataStore = PreferenceDataStoreFactory.create(
                    corruptionHandler = corruptionHandler,
                    migrations = produceMigrations(applicationContext),
                    scope = scope
                ) {
                    applicationContext.preferencesDataStoreFile(name)
                }
                scope.launch(Dispatchers.IO) {
                    val map = readBackupSharedPreferences(applicationContext, backupFileName)
                    dataStore.edit {
                        restorePreferencesFromBackup(map, it)
                    }
                }
                INSTANCE = DataStoreBackup(applicationContext, backupFileName, dataStore)
            }
            INSTANCE!!
        }
    }

    private suspend fun readBackupSharedPreferences(
        appContext: Context,
        name: String
    ): Map<String, *> {
        return withContext(Dispatchers.IO) {
            try {
                val sp = appContext.getSharedPreferences(
                    name,
                    Context.MODE_PRIVATE
                )
                sp.all
            } catch (e: Throwable) {
                emptyMap()
            }
        }
    }

    private fun restorePreferencesFromBackup(
        map: Map<String, *>,
        mutablePreferences: MutablePreferences
    ) {
        map.keys.forEach { key ->
            when (val value = map[key]) {
                is Boolean -> mutablePreferences[
                    booleanPreferencesKey(key)
                ] = value

                is Float -> mutablePreferences[
                    floatPreferencesKey(key)
                ] = value

                is Int -> mutablePreferences[
                    intPreferencesKey(key)
                ] = value

                is Long -> mutablePreferences[
                    longPreferencesKey(key)
                ] = value

                is String -> mutablePreferences[
                    stringPreferencesKey(key)
                ] = value

                is Set<*> -> {
                    @Suppress("UNCHECKED_CAST")
                    mutablePreferences[
                        stringSetPreferencesKey(key)
                    ] = value as Set<String>
                }
            }
        }
    }
}

使用示例:

val Context.userSettingsDataStore by preferencesDataStoreAndBackup("user_settings")

data class UserSettings(val isDark: Boolean)

class UserSettingsRepository(context: Context) {


    private val dataStore = context.userSettingsDataStore

    private object PreferencesKeys {

        val KEY_DARK = booleanPreferencesKey("is_dark")
    }

    val userSettingsFlow = dataStore.data.catch { ex ->
        if (ex is IOException) {
            emit(emptyPreferences())
        } else {
            throw ex
        }
    }.map {
        it[PreferencesKeys.KEY_DARK] ?: false
    }

    suspend fun setThemeDark(dark: Boolean) {
        dataStore.edit {
            it[PreferencesKeys.KEY_DARK] = dark
        }
    }
}

没错,代码方面的改动并不大呢。仅仅是把原本使用的 preferencesDataStore 替换成 preferencesDataStoreAndBackup 就行了,操作起来还挺简单的。快去测试一下,看看在经历断电重启这种情况后,数据到底能不能够成功存储,希望这个临时的解决办法能够帮你到你呢。


总结:

这种解决办法呢,确实存在一些缺点。
先说缺点的方面吧,它会导致双倍的存储时间,毕竟要同时往 DataStoreSharedPreferences 里存储数据呀,这无疑增加了数据存储所耗费的时长。不过好在它不会阻塞 UI,无论是读取数据还是写入数据,都是在协程中完成的,所以在操作过程中,用户界面不会出现卡顿之类的糟糕体验,这一点还是比较让人欣慰的。
而说到优点嘛,暂时还真没怎么发现呢,也不确定它到底有没有其他突出的优势,目前来看,它最大的作用就是解决了在立即断电重启的场景下数据无法存储的问题,从这个角度讲,也算是达到了我想要的最基本的效果了。
真心希望官方能够早点推出优化后的版本呀,这样就不用再采用这种临时的、略显笨拙的解决办法了。要是路过的大神们察觉到这个办法存在什么问题,还请不吝赐教呀,我就是个小白,很多地方还不太懂,要是能得到大家的指点,那可就太幸运了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/943427.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PH热榜 | 2024-12-23

1. Websparks 标语&#xff1a;让你的创意变为现实的AI软件工程师 介绍&#xff1a;现在&#xff0c;构建网页应用从未如此简单快捷&#xff01;WebSparks是一个基于人工智能的平台&#xff0c;它能让开发者、设计师&#xff0c;甚至不懂编程的人&#xff0c;都能在很短的时间…

Opencv之对图片的处理和运算

Opencv实现对图片的处理和修改 目录 Opencv实现对图片的处理和修改灰度图读取灰度图转换灰度图 RBG图单通道图方法一方法二 单通道图显色合并单通道图 图片截取图片打码图片组合缩放格式1格式2 图像运算图像ma[m:n,x:y]b[m1:n1,x1:y1] add加权运算 灰度图 读取灰度图 imread(‘…

OpenLinkSaas使用手册-Git工具

在OpenLinkSaas的工具箱里面&#xff0c;最基础的一个就是Git仓库管理。Git仓库功能让git使用更加简单和强大&#xff0c;不仅可以使用常规的commit/pull/push/branch等功能外&#xff0c;还连接了Git仓库供应商的能力。 OpenLinkSass支持使用国内主流的Git仓库供应商的账号登录…

WebRTC服务质量(12)- Pacer机制(04) 向Pacer中插入数据

WebRTC服务质量&#xff08;01&#xff09;- Qos概述 WebRTC服务质量&#xff08;02&#xff09;- RTP协议 WebRTC服务质量&#xff08;03&#xff09;- RTCP协议 WebRTC服务质量&#xff08;04&#xff09;- 重传机制&#xff08;01) RTX NACK概述 WebRTC服务质量&#xff08;…

protobuf学习使用

1、概述 protobuf是Google开发的一种语言中立、平台无关、可扩展的序列化结构数据格式。允许定义一次数据结构&#xff0c;然后可以使用各种支持的语言来生成代码&#xff0c;以轻松地读写这些结构到一个二进制流中&#xff0c;如网络传输或文件&#xff0c;Protobuf支持多种编…

CTFHUB-web进阶-php

我们用蚁剑中的这个插件来做这些关卡 一.LD_PRELOAD 发现这里有一句话木马&#xff0c;并且把ant给了我们&#xff0c;我们直接连接蚁剑 右键 选择模式&#xff0c;都可以试一下&#xff0c;这里第一个就可以 点击开始 我们进入到目录&#xff0c;刷新一下&#xff0c;会有一个…

相机、镜头参数详解以及相关计算公式

一、工业相机参数 1、分辨率 相机每次采集图像的像素点数&#xff0c;也是指这个相机总共有多少个感光晶片。在采集图像时&#xff0c;相机的分辨率对检测精度有很大的影响&#xff0c;在对同样打的视场成像时&#xff0c;分辨率越高&#xff0c;对细节的展示越明显。 相机像素…

取多个集合的交集

1.我们取多个集合的交集&#xff0c;先把各个集合放入list中 List < Set < String > > listnew ArrayList<>();HashSet<String> set1new HashSet<>();set1.add( "A" );set1.add("B" );set1.add("C" );HashSet<…

leetcode之hot100---206环形链表(C++)

思路一&#xff1a;哈希表 遍历链表&#xff0c;同时借助哈希表判断当前遍历到的节点是否已经被访问过&#xff0c;如果当前节点已被访问过&#xff0c;则说明存在环 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* L…

文档解析丨高效准确的PDF解析工具,赋能企业非结构化数据治理

在数据为王的时代浪潮中&#xff0c;企业数据治理已成为组织优化运营、提高竞争力的关键。随着数字化进程的加速&#xff0c;企业所积累的数据量呈爆炸式增长&#xff0c;数据类型也愈发多样化&#xff0c;这些数据构成了现代企业数据资产的重要组成部分。 然而&#xff0c;传…

优化 invite_codes 表的 SQL 创建语句

-- auto-generated definition create table invite_codes (id int auto_incrementprimary key,invite_code varchar(6) not null comment 邀请码&#xff0c;6位整数&#xff0c;确保在有效期内…

C语言基础:指针(数组指针与指针数组)

数组指针与指针数组 数组指针 概念&#xff1a;数组指针是指向数组的指针&#xff0c;本质上还是指针 特点&#xff1a; 先有数组&#xff0c;后有指针 它指向的是一个完整的数组 一维数组指针&#xff1a; 语法&#xff1a; 数据类型 (*指针变量名)[行容量][列容量]; 案…

进阶篇(1)

一.存储引擎: <1>MySQL体系结构: 1.连接层: 主要接收客户端的连接,完成一些连接的处理、认证授权、及相关操作安全方案、检测是否超过最大连接数等等;也会为安全接入的每个客户端验证它所具有的操作权限。 例如:在连接MySQL服务器时,我们需要输入用户名和密码,输…

电脑提示报错NetLoad.dll文件丢失或损坏?是什么原因?

一、NetLoad.dll文件丢失或损坏的根源 程序安装不完整&#xff1a;某些程序在安装过程中可能因为磁盘错误、网络中断或安装程序本身的缺陷&#xff0c;导致NetLoad.dll文件未能正确安装或复制。 恶意软件攻击&#xff1a;病毒、木马等恶意软件可能会篡改或删除系统文件&#x…

uniapp使用live-pusher实现模拟人脸识别效果

需求&#xff1a; 1、前端实现模拟用户人脸识别&#xff0c;识别成功后抓取视频流或认证的一张静态图给服务端。 2、服务端调用第三方活体认证接口&#xff0c;验证前端传递的人脸是否存在&#xff0c;把认证结果反馈给前端。 3、前端根据服务端返回的状态&#xff0c;显示在…

UE5仿漫威争锋灵蝶冲刺技能

这两天玩了一下漫威争锋Marvel Rivals&#xff0c;发现是UE5做的&#xff0c;对里面一些角色技能挺感兴趣的&#xff0c;想简单复刻一下技能功能&#xff0c;顺便复习一下学过的知识 首先把摄像机设置调整一下 CameraBoom里搜索lag 把摄像机延迟关掉 &#xff0c;这样摄像机就…

去除 el-input 输入框的边框(element-ui@2.15.13)

dgqdgqdeMac-mini spid-admin % yarn list --pattern element-ui yarn list v1.22.22 └─ element-ui2.15.13 ✨ Done in 0.23s.dgqdgqdeMac-mini spid-admin % yarn list vue yarn list v1.22.22 warning Filtering by arguments is deprecated. Please use the pattern opt…

Suno Api V4模型无水印开发「综合实战开发自己的音乐网站」 —— 「Suno Api系列」第14篇

历史文章 Suno AI API接入 - 将AI音乐接入到自己的产品中&#xff0c;支持120并发任务 Suno Api V4模型无水印开发「灵感模式」 —— 「Suno Api系列」第1篇 Suno Api V4模型无水印开发「自定义模式」 —— 「Suno Api系列」第2篇 Suno Api V4模型无水印开发「AI生成歌词」…

企业如何搭建安全的跨网文件安全交换管理系统

在数字化转型的浪潮中&#xff0c;企业对数据的安全性和流动性提出了前所未有的高要求。特别是在网络隔离的情况下&#xff0c;如何实现跨网的安全、高效的文件交换成为了众多企业迫切需要解决的问题。 这不仅是技术上的挑战&#xff0c;还涉及到企业内部管理流程的优化和安全策…

Torch.gather

1.官方文档 2.使用要点 输入index的shape等于输出value的shape输入index的索引值仅替换该index中对应dim的index值最终输出为替换index后在原tensor中的值 最终输出的shape和index的shape相同 根据dim的值 选择将index[i,j,k]这个结果替换input[i,j,k]里面对应的i or j or…