使用 Android 备份服务备份键值对

Android 备份服务为您的 Android 应用中的键值数据提供云存储备份和恢复功能。在键值备份操作期间,应用的数据备份会传递给设备的备份传输服务。如果设备使用默认的 Google 备份传输服务,则数据会传递给 Android 备份服务进行归档。

每个应用用户的数据量限制为 5MB。存储备份数据不收取费用。

有关 Android 备份选项的概览以及关于应备份和恢复哪些数据的指导,请参阅数据备份概览

实现键值备份

要备份您的应用数据,您需要实现一个备份代理。备份代理在备份和恢复过程中都会由 Backup Manager 调用。

要实现备份代理,您必须:

  1. 在您的 manifest 文件中,使用 android:backupAgent 属性声明您的备份代理。

  2. 通过以下其中一种方式定义备份代理:

    • 扩展 BackupAgent

      BackupAgent 类提供了您的应用用于与 Backup Manager 通信的中心接口。如果您直接扩展此类,则必须覆盖 onBackup()onRestore() 来处理您的数据备份和恢复操作。

    • 扩展 BackupAgentHelper

      BackupAgentHelper 类提供了 BackupAgent 类便捷的封装器,最大限度地减少您需要编写的代码量。在您的 BackupAgentHelper 中,您必须使用一个或多个 helper 对象,这些对象会自动备份和恢复某些类型的数据,因此您无需实现 onBackup()onRestore()。除非您需要对应用的备份拥有完全控制权,否则建议使用 BackupAgentHelper 来处理应用的备份。

      Android 目前提供了备份助手,可以备份和恢复来自 SharedPreferences内部存储的完整文件。

在 manifest 中声明备份代理

一旦您确定了备份代理的类名,就可以在 <application> 标签中使用 android:backupAgent 属性在 manifest 中声明它。

例如:

<manifest ... >
    ...
    <application android:label="MyApplication"
                 android:backupAgent="MyBackupAgent">
        <meta-data android:name="com.google.android.backup.api_key"
            android:value="unused" />
        <activity ... >
            ...
        </activity>
    </application>
</manifest>

为了支持旧设备,我们建议在 Android manifest 文件中添加 API 密钥 <meta-data>。Android 备份服务不再需要服务密钥,但某些旧设备在备份时可能仍会检查是否存在密钥。将 android:name 设置为 com.google.android.backup.api_key,将 android:value 设置为 unused

android:restoreAnyVersion 属性接受一个布尔值,指示您是否希望无论当前应用版本与生成备份数据的版本如何,都恢复应用数据。默认值为 false。有关详细信息,请参阅检查恢复数据版本

扩展 BackupAgentHelper

如果您想备份来自 SharedPreferences 或内部存储的完整文件,则应使用 BackupAgentHelper 构建您的备份代理。使用 BackupAgentHelper 构建备份代理所需的代码量远少于扩展 BackupAgent,因为您无需实现 onBackup()onRestore()

您的 BackupAgentHelper 实现必须使用一个或多个备份助手。备份助手是一个专门的组件,BackupAgentHelper 调用它来对特定类型的数据执行备份和恢复操作。Android 框架目前提供了两种不同的助手:

您可以在 BackupAgentHelper 中包含多个助手,但每种数据类型只需一个助手。也就是说,如果您有多个 SharedPreferences 文件,则只需要一个 SharedPreferencesBackupHelper

对于您要添加到 BackupAgentHelper 的每个助手,您必须在 onCreate() 方法中执行以下操作:

  1. 实例化所需助手类的实例。在类构造函数中,您必须指定要备份的文件。
  2. 调用 addHelper() 将助手添加到您的 BackupAgentHelper

以下部分介绍了如何使用每个可用助手创建备份代理。

备份 SharedPreferences

当您实例化 SharedPreferencesBackupHelper 时,您必须包含一个或多个 SharedPreferences 文件的名称。

例如,要备份名为 user_preferencesSharedPreferences 文件,使用 BackupAgentHelper 的完整备份代理如下所示:

Kotlin

// The name of the SharedPreferences file
const val PREFS = "user_preferences"

// A key to uniquely identify the set of backup data
const val PREFS_BACKUP_KEY = "prefs"

class MyPrefsBackupAgent : BackupAgentHelper() {
    override fun onCreate() {
        // Allocate a helper and add it to the backup agent
        SharedPreferencesBackupHelper(this, PREFS).also {
            addHelper(PREFS_BACKUP_KEY, it)
        }
    }
}

Java

public class MyPrefsBackupAgent extends BackupAgentHelper {
    // The name of the SharedPreferences file
    static final String PREFS = "user_preferences";

    // A key to uniquely identify the set of backup data
    static final String PREFS_BACKUP_KEY = "prefs";

    // Allocate a helper and add it to the backup agent
    @Override
    public void onCreate() {
        SharedPreferencesBackupHelper helper =
                new SharedPreferencesBackupHelper(this, PREFS);
        addHelper(PREFS_BACKUP_KEY, helper);
    }
}

SharedPreferencesBackupHelper 包含了备份和恢复 SharedPreferences 文件所需的所有代码。

当 Backup Manager 调用 onBackup()onRestore() 时,BackupAgentHelper 会调用您的备份助手来备份和恢复您指定的文件。

备份其他文件

当您实例化 FileBackupHelper 时,您必须包含一个或多个文件名称,这些文件保存在您应用的内部存储中,如 getFilesDir() 指定的位置,该位置与 openFileOutput() 写入文件的位置相同。

例如,要备份两个名为 scoresstats 的文件,使用 BackupAgentHelper 的备份代理如下所示:

Kotlin

// The name of the file
const val TOP_SCORES = "scores"
const val PLAYER_STATS = "stats"
// A key to uniquely identify the set of backup data
const val FILES_BACKUP_KEY = "myfiles"

class MyFileBackupAgent : BackupAgentHelper() {
    override fun onCreate() {
        // Allocate a helper and add it to the backup agent
        FileBackupHelper(this, TOP_SCORES, PLAYER_STATS).also {
            addHelper(FILES_BACKUP_KEY, it)
        }
    }
}

Java

public class MyFileBackupAgent extends BackupAgentHelper {
    // The name of the file
    static final String TOP_SCORES = "scores";
    static final String PLAYER_STATS = "stats";

    // A key to uniquely identify the set of backup data
    static final String FILES_BACKUP_KEY = "myfiles";

    // Allocate a helper and add it to the backup agent
    @Override
    public void onCreate() {
        FileBackupHelper helper = new FileBackupHelper(this,
                TOP_SCORES, PLAYER_STATS);
        addHelper(FILES_BACKUP_KEY, helper);
    }
}

FileBackupHelper 包含了备份和恢复保存在应用内部存储中的文件所需的所有代码。

但是,对内部存储上的文件的读写是非线程安全的。为确保您的备份代理不会与您的 activity 同时读取或写入文件,您必须在每次执行读写操作时使用同步语句。例如,在您读取和写入文件的任何 activity 中,您需要一个对象作为同步语句的固有锁:

Kotlin

// Object for intrinsic lock
companion object {
    val sDataLock = Any()
}

Java

// Object for intrinsic lock
static final Object sDataLock = new Object();

然后,每次读取或写入文件时,使用此锁创建一个同步语句。例如,这是一个同步语句,用于将游戏中的最新得分写入文件:

Kotlin

try {
    synchronized(MyActivity.sDataLock) {
        val dataFile = File(filesDir, TOP_SCORES)
        RandomAccessFile(dataFile, "rw").apply {
            writeInt(score)
        }
    }
} catch (e: IOException) {
    Log.e(TAG, "Unable to write to file")
}

Java

try {
    synchronized (MyActivity.sDataLock) {
        File dataFile = new File(getFilesDir(), TOP_SCORES);
        RandomAccessFile raFile = new RandomAccessFile(dataFile, "rw");
        raFile.writeInt(score);
    }
} catch (IOException e) {
    Log.e(TAG, "Unable to write to file");
}

您应该使用相同的锁同步您的读取语句。

然后,在您的 BackupAgentHelper 中,您必须覆盖 onBackup()onRestore() 以使用相同的固有锁同步备份和恢复操作。例如,上面提到的 MyFileBackupAgent 示例需要以下方法:

Kotlin

@Throws(IOException::class)
override fun onBackup(
        oldState: ParcelFileDescriptor,
        data: BackupDataOutput,
        newState: ParcelFileDescriptor
) {
    // Hold the lock while the FileBackupHelper performs back up
    synchronized(MyActivity.sDataLock) {
        super.onBackup(oldState, data, newState)
    }
}

@Throws(IOException::class)
override fun onRestore(
        data: BackupDataInput,
        appVersionCode: Int,
        newState: ParcelFileDescriptor
) {
    // Hold the lock while the FileBackupHelper restores the file
    synchronized(MyActivity.sDataLock) {
        super.onRestore(data, appVersionCode, newState)
    }
}

Java

@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
          ParcelFileDescriptor newState) throws IOException {
    // Hold the lock while the FileBackupHelper performs back up
    synchronized (MyActivity.sDataLock) {
        super.onBackup(oldState, data, newState);
    }
}

@Override
public void onRestore(BackupDataInput data, int appVersionCode,
        ParcelFileDescriptor newState) throws IOException {
    // Hold the lock while the FileBackupHelper restores the file
    synchronized (MyActivity.sDataLock) {
        super.onRestore(data, appVersionCode, newState);
    }
}

扩展 BackupAgent

大多数应用不应直接扩展 BackupAgent 类,而应 扩展 BackupAgentHelper 以利用内置的助手类自动备份和恢复您的文件。但是,您可以直接扩展 BackupAgent 以执行以下操作:

  • 版本化您的数据格式。例如,如果您预期需要修改写入应用数据的格式,则可以构建一个备份代理来在恢复操作期间交叉检查您的应用版本,并在设备上的版本与备份数据版本不同时执行任何必要的兼容性工作。有关详细信息,请参阅检查恢复数据版本
  • 指定要备份的数据部分。您可以指定要备份的数据部分以及每部分如何恢复到设备,而不是备份整个文件。这也可以帮助您管理不同版本,因为您将数据作为独立实体而不是完整文件进行读写。
  • 备份数据库中的数据。如果您有一个 SQLite 数据库,并且希望在用户重新安装应用时恢复它,您需要构建一个自定义的 BackupAgent,在备份操作期间读取适当的数据,然后在恢复操作期间创建您的表并插入数据。

如果您不需要执行上述任何任务,并且想要备份来自 SharedPreferences 或内部存储的完整文件,请参阅扩展 BackupAgentHelper

所需方法

创建 BackupAgent 时,您必须实现以下回调方法:

onBackup()
在您请求备份后,Backup Manager 调用此方法。在此方法中,您从设备读取您的应用数据,并将要备份的数据传递给 Backup Manager,如执行备份中所述。
onRestore()

在恢复操作期间,Backup Manager 调用此方法。此方法会传递您的备份数据,您的应用可以使用这些数据恢复其之前的状态,如执行恢复中所述。

当用户重新安装您的应用时,系统会调用此方法来恢复任何备份数据,但您的应用也可以请求恢复

执行备份

备份请求不会立即调用您的 onBackup() 方法。相反,Backup Manager 会等待适当的时间,然后对自上次备份以来所有已请求备份的应用执行备份。此时,您必须向 Backup Manager 提供您的应用数据,以便将其保存到云存储中。

只有 Backup Manager 可以调用您的备份代理的 onBackup() 方法。每当您的应用数据更改并想要执行备份时,您必须通过调用 dataChanged() 来请求备份操作。有关详细信息,请参阅请求备份

提示:在开发应用时,您可以使用 bmgr 工具从 Backup Manager 启动立即备份操作。

当 Backup Manager 调用您的 onBackup() 方法时,它会传递三个参数:

oldState
一个打开的只读 ParcelFileDescriptor,指向您的应用提供的上次备份状态。这不是云存储中的备份数据,而是上次调用 onBackup() 时备份的数据的本地表示,由 newStateonRestore() 定义。onRestore() 在下一节介绍。由于 onBackup() 不允许您读取云存储中的现有备份数据,因此您可以使用此本地表示来确定自上次备份以来您的数据是否已更改。
data
一个 BackupDataOutput 对象,您可以使用它将备份数据传递给 Backup Manager。
newState
一个打开的读写 ParcelFileDescriptor,指向一个文件,您必须在该文件中写入您传递给 data 的数据的表示。表示可以像文件的上次修改时间戳一样简单。下次 Backup Manager 调用您的 onBackup() 方法时,此对象将作为 oldState 返回。如果您没有将备份数据写入 newState,则下次 Backup Manager 调用 onBackup() 时,oldState 将指向一个空文件。

使用这些参数,实现您的 onBackup() 方法以执行以下操作:

  1. 通过将 oldState 与当前数据进行比较,检查自上次备份以来您的数据是否已更改。如何读取 oldState 中的数据取决于您最初如何将其写入 newState(请参阅步骤 3)。记录文件状态的最简单方法是使用其上次修改时间戳。例如,这里是如何读取和比较来自 oldState 的时间戳:

    Kotlin

    val instream = FileInputStream(oldState.fileDescriptor)
    val dataInputStream = DataInputStream(instream)
    try {
       // Get the last modified timestamp from the state file and data file
       val stateModified = dataInputStream.readLong()
       val fileModified: Long = dataFile.lastModified()
       if (stateModified != fileModified) {
           // The file has been modified, so do a backup
           // Or the time on the device changed, so be safe and do a backup
       } else {
           // Don't back up because the file hasn't changed
           return
       }
    } catch (e: IOException) {
       // Unable to read state file... be safe and do a backup
    }

    Java

    // Get the oldState input stream
    FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
    DataInputStream in = new DataInputStream(instream);
    
    try {
       // Get the last modified timestamp from the state file and data file
       long stateModified = in.readLong();
       long fileModified = dataFile.lastModified();
    
       if (stateModified != fileModified) {
           // The file has been modified, so do a backup
           // Or the time on the device changed, so be safe and do a backup
       } else {
           // Don't back up because the file hasn't changed
           return;
       }
    } catch (IOException e) {
       // Unable to read state file... be safe and do a backup
    }

    如果没有任何变化,并且您不需要备份,请跳到步骤 3。

  2. 如果您的数据与 oldState 相比已更改,请将当前数据写入 data 以将其备份到云存储中。

    您必须将每块数据作为 BackupDataOutput 中的一个实体写入。实体是一个由唯一键字符串标识的扁平化二进制数据记录。因此,您备份的数据集在概念上是一组键值对。

    要将实体添加到您的备份数据集,您必须:

    1. 调用 writeEntityHeader(),传递您要写入的数据的唯一字符串键和数据大小。

    2. 调用 writeEntityData(),传递包含您数据的字节缓冲区以及要从缓冲区写入的字节数,该字节数应与传递给 writeEntityHeader() 的大小匹配。

    例如,以下代码将某些数据扁平化为字节流并将其写入单个实体:

    Kotlin

    val buffer: ByteArray = ByteArrayOutputStream().run {
       DataOutputStream(this).apply {
           writeInt(playerName)
           writeInt(playerScore)
       }
       toByteArray()
    }
    val len: Int = buffer.size
    data.apply {
       writeEntityHeader(TOPSCORE_BACKUP_KEY, len)
       writeEntityData(buffer, len)
    }

    Java

    // Create buffer stream and data output stream for our data
    ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
    DataOutputStream outWriter = new DataOutputStream(bufStream);
    // Write structured data
    outWriter.writeUTF(playerName);
    outWriter.writeInt(playerScore);
    // Send the data to the Backup Manager via the BackupDataOutput
    byte[] buffer = bufStream.toByteArray();
    int len = buffer.length;
    data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len);
    data.writeEntityData(buffer, len);

    对您要备份的每个数据块执行此操作。如何将数据分割为实体取决于您。您甚至可以只使用一个实体。

  3. 无论您是否执行备份(在步骤 2 中),都将当前数据的表示写入 newState ParcelFileDescriptor。Backup Manager 会在本地保留此对象,作为当前已备份数据的表示。下次调用 onBackup() 时,它会将其作为 oldState 传递回给您,以便您可以确定是否需要另一次备份,如步骤 1 中处理的那样。如果您不将当前数据状态写入此文件,则在下次回调期间,oldState 将为空。

    以下示例使用文件的上次修改时间戳将当前数据的表示保存到 newState 中:

    Kotlin

    val modified = dataFile.lastModified()
    FileOutputStream(newState.fileDescriptor).also {
       DataOutputStream(it).apply {
           writeLong(modified)
       }
    }

    Java

    FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
    DataOutputStream out = new DataOutputStream(outstream);
    
    long modified = dataFile.lastModified();
    out.writeLong(modified);

执行恢复

当需要恢复您的应用数据时,Backup Manager 会调用您的备份代理的 onRestore() 方法。调用此方法时,Backup Manager 会传递您的备份数据,以便您可以将其恢复到设备上。

只有 Backup Manager 可以调用 onRestore(),当系统安装您的应用并找到现有备份数据时,会自动发生这种情况。

当 Backup Manager 调用您的 onRestore() 方法时,它会传递三个参数:

data
一个 BackupDataInput 对象,它允许您读取备份数据。
appVersionCode
一个整数,表示您的应用清单文件的 android:versionCode 属性值,即备份此数据时的值。您可以使用此属性来交叉检查当前应用版本并确定数据格式是否兼容。有关使用此属性处理不同版本恢复数据的详细信息,请参阅检查恢复数据版本
newState
一个打开的读写 ParcelFileDescriptor,指向一个文件,您必须在该文件中写入随 data 提供最终备份状态。下次调用 onBackup() 时,此对象将作为 oldState 返回。请记住,您还必须在 onBackup() 回调期间写入相同的 newState 对象——在这里执行此操作还可以确保即使在设备恢复后第一次调用 onBackup() 时,提供给 onBackup()oldState 对象也是有效的。

在您的 onRestore() 实现中,您应该对 data 调用 readNextHeader() 以遍历数据集中的所有实体。对于找到的每个实体,执行以下操作:

  1. 使用 getKey() 获取实体键。
  2. 将实体键与您应在 BackupAgent 类中声明为静态 final 字符串的已知键值列表进行比较。当键匹配您的已知键字符串之一时,进入一个语句块以提取实体数据并将其保存到设备:

    1. 使用 getDataSize() 获取实体数据大小,并创建相应大小的字节数组。
    2. 调用 readEntityData() 并向其传递字节数组(数据将存储在此处),并指定起始偏移量和要读取的大小。
    3. 您的字节数组现在已满。读取数据并以您喜欢的方式将其写入设备。
  3. 将数据读回并写入设备后,像在 onBackup() 中一样,将数据状态写入 newState 参数。

例如,这里是如何恢复上一节示例中备份的数据:

Kotlin

@Throws(IOException::class)
override fun onRestore(data: BackupDataInput, appVersionCode: Int,
                       newState: ParcelFileDescriptor) {
    with(data) {
        // There should be only one entity, but the safest
        // way to consume it is using a while loop
        while (readNextHeader()) {
            when(key) {
                TOPSCORE_BACKUP_KEY -> {
                    val dataBuf = ByteArray(dataSize).also {
                        readEntityData(it, 0, dataSize)
                    }
                    ByteArrayInputStream(dataBuf).also {
                        DataInputStream(it).apply {
                            // Read the player name and score from the backup data
                            playerName = readUTF()
                            playerScore = readInt()
                        }
                        // Record the score on the device (to a file or something)
                        recordScore(playerName, playerScore)
                    }
                }
                else -> skipEntityData()
            }
        }
    }

    // Finally, write to the state blob (newState) that describes the restored data
    FileOutputStream(newState.fileDescriptor).also {
        DataOutputStream(it).apply {
            writeUTF(playerName)
            writeInt(mPlayerScore)
        }
    }
}

Java

@Override
public void onRestore(BackupDataInput data, int appVersionCode,
                      ParcelFileDescriptor newState) throws IOException {
    // There should be only one entity, but the safest
    // way to consume it is using a while loop
    while (data.readNextHeader()) {
        String key = data.getKey();
        int dataSize = data.getDataSize();

        // If the key is ours (for saving top score). Note this key was used when
        // we wrote the backup entity header
        if (TOPSCORE_BACKUP_KEY.equals(key)) {
            // Create an input stream for the BackupDataInput
            byte[] dataBuf = new byte[dataSize];
            data.readEntityData(dataBuf, 0, dataSize);
            ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
            DataInputStream in = new DataInputStream(baStream);

            // Read the player name and score from the backup data
            playerName = in.readUTF();
            playerScore = in.readInt();

            // Record the score on the device (to a file or something)
            recordScore(playerName, playerScore);
        } else {
            // We don't know this entity key. Skip it. (Shouldn't happen.)
            data.skipEntityData();
        }
    }

    // Finally, write to the state blob (newState) that describes the restored data
    FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
    DataOutputStream out = new DataOutputStream(outstream);
    out.writeUTF(playerName);
    out.writeInt(mPlayerScore);
}

在此示例中,传递给 onRestore()appVersionCode 参数未使用。但是,如果您选择在用户应用版本实际回退时(例如,用户从应用的 1.5 版本降级到 1.0 版本)执行备份,则可能需要使用它。有关详细信息,请参阅下一节。

检查恢复数据版本

当 Backup Manager 将您的数据保存到云存储时,它会自动包含您的应用版本,如清单文件的 android:versionCode 属性所定义。在 Backup Manager 调用您的备份代理来恢复您的数据之前,它会查看已安装应用的 android:versionCode,并将其与恢复数据集中记录的值进行比较。如果恢复数据集中记录的版本比设备上应用版本,则表示用户已降级其应用。在这种情况下,Backup Manager 将终止您应用的恢复操作,并且不会调用您的 onRestore() 方法,因为恢复集被认为对旧版本没有意义。

您可以使用 android:restoreAnyVersion 属性覆盖此行为。将此属性设置为 true 表示无论恢复集版本如何,您都希望恢复应用。默认值为 false。如果您将此设置为 true,则 Backup Manager 将忽略 android:versionCode 并始终调用您的 onRestore() 方法。这样,您可以在 onRestore() 方法中手动检查版本差异,并在版本不匹配时采取任何必要的步骤使数据兼容。

为了帮助您在恢复操作期间处理不同版本,onRestore() 方法将恢复数据集中包含的版本代码作为 appVersionCode 参数传递给您。然后,您可以使用 PackageInfo.versionCode 字段查询当前应用的版本代码。例如:

Kotlin

val info: PackageInfo? = try {
    packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
    null
}

val version: Int = info?.versionCode ?: 0

Java

PackageInfo info;
try {
    String name = getPackageName();
    info = getPackageManager().getPackageInfo(name, 0);
} catch (NameNotFoundException nnfe) {
    info = null;
}

int version;
if (info != null) {
    version = info.versionCode;
}

然后,将从 PackageInfo 获取的 version 与传递给 onRestore()appVersionCode 进行比较。

请求备份

您可以通过随时调用 dataChanged() 来请求备份操作。此方法通知 Backup Manager 您希望使用您的备份代理备份数据。Backup Manager 随后会在将来的某个时间调用您的备份代理的 onBackup() 方法。通常,您应该在每次数据更改时(例如,当用户更改您希望备份的应用首选项时)请求备份。如果您在 Backup Manager 请求从您的代理进行备份之前多次调用 dataChanged(),您的代理仍只会收到一次对 onBackup() 的调用。

请求恢复

在应用的正常生命周期中,您无需请求恢复操作。系统会在安装您的应用时自动检查备份数据并执行恢复。

迁移到自动备份

您可以通过在 manifest 文件中的 <application> 元素中将 android:fullBackupOnly 设置为 true 来将您的应用迁移到全数据备份。在运行 Android 5.1(API 级别 22)或更低版本的设备上时,您的应用会忽略 manifest 中的此值,并继续执行键值备份。在运行 Android 6.0(API 级别 23)或更高版本的设备上时,您的应用会执行自动备份而不是键值备份。

用户隐私

在 Google,我们深知用户对我们的信任以及我们保护用户隐私的责任。Google 会安全地传输备份数据到 Google 服务器以及从 Google 服务器传输备份数据,以提供备份和恢复功能。Google 根据 Google 的隐私权政策将这些数据视为个人信息。

此外,用户可以通过 Android 系统的备份设置禁用数据备份功能。当用户禁用备份时,Android 备份服务会删除所有已保存的备份数据。用户可以在设备上重新启用备份,但 Android 备份服务不会恢复任何先前已删除的数据。