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

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

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

有关 Android 备份选项的概述以及有关应备份和还原哪些数据的指南,请参阅 数据备份概述

实现键值备份

要备份您的应用数据,您需要实现备份代理。备份管理器在备份和还原期间都会调用您的备份代理。

要实现备份代理,您必须

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

  2. 通过执行以下操作之一定义备份代理

    • 扩展 BackupAgent

      BackupAgent 类提供了您的应用用于与备份管理器通信的核心接口。如果直接扩展此类,则必须重写 onBackup()onRestore() 以处理数据的备份和还原操作。

    • 扩展 BackupAgentHelper

      BackupAgentHelper 类提供了围绕 BackupAgent 类的便捷包装器,从而最大程度地减少了您需要编写的代码量。在您的 BackupAgentHelper 中,您必须使用一个或多个辅助对象,这些对象会自动备份和还原某些类型的数据,这样您就不需要实现 onBackup()onRestore()。除非您需要完全控制应用的备份,否则建议使用 BackupAgentHelper 来处理应用的备份。

      Android 目前提供备份辅助程序,这些程序将备份和还原来自 SharedPreferences内部存储 的完整文件。

在清单中声明备份代理

确定备份代理的类名后,请使用 <application> 标记中的 android:backupAgent 属性在清单中声明它。

例如

<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>

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

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

扩展 BackupAgentHelper

如果您想从 SharedPreferences 或内部存储中备份完整文件,则应使用 BackupAgentHelper 构建您的备份代理。使用 BackupAgentHelper 构建备份代理所需的代码要少得多,因为您不必实现 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 文件所需的所有代码。

当备份管理器调用 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 包含备份和恢复保存到应用内部存储的文件所需的所有代码。

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

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()
在您 请求备份 后,备份管理器会调用此方法。在此方法中,您从设备读取应用数据,并将要备份的数据传递给备份管理器,如 执行备份 中所述。
onRestore()

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

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

执行备份

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

只有备份管理器才能调用备份代理的 onBackup() 方法。每次应用数据发生更改并且您想要执行备份时,都必须通过调用 dataChanged() 来请求备份操作。有关更多信息,请参阅 请求备份

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

当备份管理器调用您的 onBackup() 方法时,它会传递三个参数

oldState
指向应用提供的上次备份状态的打开的只读 ParcelFileDescriptor。这不是来自云存储的备份数据,而是上次调用 onBackup() 时备份的数据的本地表示形式,如 newState 或来自 onRestore() 所定义。下一节将介绍 onRestore()。由于 onBackup() 不允许您读取云存储中现有的备份数据,因此您可以使用此本地表示形式来确定自上次备份以来数据是否已更改。
data
一个 BackupDataOutput 对象,您可以使用它将备份数据传递给备份管理器。
newState
指向一个文件的打开的读写 ParcelFileDescriptor,您必须在此文件中写入传递给 data 的数据的表示形式。表示形式可以像文件的最后修改时间戳一样简单。此对象在下次备份管理器调用您的 onBackup() 方法时作为 oldState 返回。如果您没有将备份数据写入 newState,则下次备份管理器调用 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。备份管理器会在本地保留此对象作为当前备份数据的表示。在下次调用 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);
    

执行恢复

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

只有备份管理器才能调用 onRestore(),这会在系统安装您的应用并找到现有备份数据时自动发生。

当备份管理器调用您的 onRestore() 方法时,它会传递三个参数。

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

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

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

    1. 使用 getDataSize() 获取实体数据大小,并创建该大小的字节数组。
    2. 调用 readEntityData() 并将字节数组传递给它,数据将存储在该数组中,并指定开始偏移量和要读取的大小。
    3. 您的字节数组现在已满。读取数据并以您喜欢的任何方式将其写入设备。
  3. 在将数据读回并写入设备后,将数据的状态写入 newState 参数,就像在 onBackup() 中所做的那样。

例如,以下是如何恢复上一节中示例备份的数据。

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 版),则您可能希望使用它。有关更多信息,请参阅下一节。

检查恢复数据版本

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

您可以使用 android:restoreAnyVersion 属性覆盖此行为。将此属性设置为 true 以指示您希望无论恢复集版本如何都恢复应用。默认值为 false。如果将其设置为 true,则备份管理器将忽略 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() 请求备份操作。此方法通知备份管理器您希望使用备份代理备份您的数据。然后,备份管理器会在将来的某个时间调用备份代理的 onBackup() 方法。通常,您应该在每次数据更改时(例如,当用户更改您要备份的应用首选项时)请求备份。如果您在备份管理器从您的代理请求备份之前多次调用 dataChanged(),您的代理仍然只会收到一次对 onBackup() 的调用。

请求恢复

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

迁移到自动备份

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

用户隐私

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

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