这个问题纠结了很久,Google 官方文档又一直在鼓吹自家的云备份,各种办法似乎最终都要跟云联系起来,找了很久总算明白了解决方法……

    这里是借助 SharedPreferencesgetAll() 方法(Kotlin 中直接 .all(Kotlin YES!)

    由于 Google 对 Android 权限的收紧,未来直接操作备份文件的方式并不可取也不行,因此我使用了 SAF。

    而且用了 SAF,也可以选择直接将备份文件备份到 Google Drive 等采用标准接口接入 SAF 的应用。

    准备

    创建变量

    为了便于后期开发的理解,我们创建两个变量:

    private val WRITE_REQUEST_CODE: Int = 43
    private val READ_REQUEST_CODE: Int = 42

    很好理解,只是为了在回调的时候判断返回的是写入文件(备份)还是读取文件(恢复)

    这是 Google 官方文档中使用的变量及变量值,这里直接沿用,但是并非一定是这俩值。

    另外,这两个变量并非一定要处于同一份文件中,请根据备份和恢复按钮的文件位置自行判断。

    引入 fastjson(可选)

    其实并不一定要引入 fastjson,只是我在我的项目中早已引入,而且 fastjson 易于使用。

    我调用 fastjson 的目的只是「将 Map 转换为 JSON(String)」以及「将 JSON(String)转换成 Map」,你当然可以用原生 JSON 库,用 Gson 来实现 但我没试过

    Github:https://github.com/alibaba/fastjson

    这里我引入的是 fastjson 的 Android 版本
    官方声称:「和标准版本相比,Android 版本去掉一些 Android 虚拟机 Dalvik 不支持的功能,使得 jar 更小,同时针对 Dalvik 做了很多性能优化,包括减少方法调用等。parse 为 JSONObject/JSONArray 时比原生 org.json 速度快,序列化反序列化 JavaBean 性能比 jackson/gson 性能更好」

    Module 级build.gradle 下的 dependencies 中加入

    implementation 'com.alibaba:fastjson:VERSION_CODE'

    版本可以直接查阅 fastjson 官方 Github 库或 Maven.orgbintray,截止到这篇文章撰写的时刻,我使用 com.alibaba:fastjson:1.1.72.android

    备份数据

    首先要明确的一点是,我们最终还是文件操作,但是不同于 File 直接传入文件路径,我们选择通过 SAF 传入 URI 来操作。

    这里,我们选择最好理解的 JSON 作为数据存储格式。

    创建文档

    首先要做的是「创建文档」,我们需要借助 startActivityForResult() 启动一个意图。

    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/json"
        putExtra(Intent.EXTRA_TITLE, "文件名")
    }
    startActivityForResult(intent, WRITE_REQUEST_CODE)

    文件的 MIME 类型请自行判断。我推荐你使用 System.currentTimeMillis() 来得到时间戳并填入文件名中,以防止备份文件名称重复。

    回调

    有了 startActivityForResult(),自然就需要回调,重写 onActivityResult()

    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        super.onActivityResult(requestCode, resultCode, resultData)
        if (requestCode == WRITE_REQUEST_CODE && resultData != null && resultData.data != null) {
            //备份
            if (backupSharedPreferences(resultData.data as Uri)) {
                //成功时
            }else{
                //失败时
            }
        }
    }

    实现

    理所当然地,我们现在需要开始写 backupSharedPreferences() 方法

    private fun backupSharedPreferences(uri: Uri): Boolean {
        var spIntent: SharedPreferences = getSharedPreferences("要备份的SP的名字", Context.MODE_PRIVATE)
        try {
            contentResolver.openFileDescriptor(uri, "w")?.use {
                FileOutputStream(it.fileDescriptor).use {
                    it.write(
                        JSON.toJSONString(spIntent.all).toByteArray()
                    )
                }
            }
            return true
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return false
    }

    这段代码其实不难理解,从 ContentResolver 获取 FileOutputStream,传入 URI 并使用默认的写入模式("w"),然后将想要的数据写入文件。

    这里我们先用 getSharedPreferences() 传入指定参数后获得一个 SharedPreferences 类型的对象,命名为 spIntent(啥都行,自己决定),目的是通过 spIntent.all 获取此 SharedPreferences 文件的全部数据(Map 类型)。

    再通过 fastjson 的 JSON.toJSONString() 方法,将 Map 转换为 JSON 字符串,最终写入 URI 对应的文件,也就是我们在 SAF 中创建的文件。

    至此,备份 SharedPreferences 数据完成。

    恢复数据

    当然了,这里恢复的数据必定得是由上面的办法备份的数据才行。

    打开文档

    与备份的「创建文档」相对应的,这里使用「打开文档」。

    同样的,我们需要借助 startActivityForResult() 启动一个意图。

    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/json"
    }
    startActivityForResult(intent, READ_REQUEST_CODE)

    回调

    同样的,我们需要回调。如果你的备份和恢复处于同一个 Activity 中,两个 if 需要同时共存于 onActivityResult() 中。重写 onActivityResult()

    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        super.onActivityResult(requestCode, resultCode, resultData)
        if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
            //还原
            if (restoreSharedPreferences(resultData.data as Uri)) {
                //成功时
            }
        }
    }

    实现

    同样理所当然地,我们现在需要开始写 restoreSharedPreferences() 方法。

    @Throws(IOException::class)
    private fun restoreSharedPreferences(uri: Uri): Boolean {
        var speIntent: SharedPreferences.Editor = getSharedPreferences("要备份的SP的名字", Context.MODE_PRIVATE).edit()
        val stringBuilder = StringBuilder()
        contentResolver.openInputStream(uri)?.use { inputStream ->
            BufferedReader(InputStreamReader(inputStream)).use { reader ->
                var line: String? = reader.readLine()
                while (line != null) {
                    stringBuilder.append(line)
                    line = reader.readLine()
                }
            }
        }
        val map = JSON.parseObject(stringBuilder.toString())
        speIntent.clear()
        map.forEach {
            speIntent.putBoolean(it.key, it.value as Boolean)
        }
        speIntent.apply()
        return true
    }

    不再过多赘述,我们一番操作后便可以从 stringBuilder.toString() 中拿到所需文件(恢复所用文件)的字符串,通过 fastjson 的 JSON.parseObject() 直接将字符串转换为 Map。

    在清除掉原有的 SharedPreferences 数据后,通过遍历 Map,写入 SharedPreferences 中并最终 apply() 提交更改。另外,putBoolean() 只是因为我的 SP 是布尔类型的,请自己判断修改。

    至此,恢复 SharedPreferences 数据完成。