这个问题纠结了很久,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 数据完成。