Android Backup Service 可为 Android 应用中的键值对数据提供云端存储空间备份和恢复服务。在键值对备份操作执行过程中,应用的备份数据会传递到设备的备份传输中。如果设备使用的是默认 Google 备份传输,则数据会传递到 Android Backup Service 进行归档。
数据上限为应用的每个用户 5MB,并且存储备份数据是免费的。
如需简要了解 Android 备份选项并获得有关您应该备份和恢复哪些数据的指南,请参阅数据备份概览。
实现键值对备份
如需备份您的应用数据,您需要实现备份代理。在备份和恢复期间,备份管理器会调用您的备份代理。
如需实现备份代理,您必须:
使用
android:backupAgent
属性在清单文件中声明备份代理。通过以下任一方式定义备份代理:
-
BackupAgent
类提供一个中央接口,您的应用使用该接口与备份管理器进行通信。如果您直接扩展此类,则必须替换onBackup()
和onRestore()
才能处理数据的备份和恢复操作。 -
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 Backup Service 不再需要服务密钥,但某些旧版设备在备份时可能仍会检查是否有密钥。将 android:name
设置为 com.google.android.backup.api_key
,并将 android:value
设置为 unused
。
android:restoreAnyVersion
属性接受布尔值,表示您是否想要恢复应用数据,无论与生成备份数据的应用版本相比,当前应用版本如何。默认值为 false
。如需了解详情,请参阅检查恢复数据版本。
扩展 BackupAgentHelper
如果您要备份 SharedPreferences
或内部存储空间中的完整文件,则应使用 BackupAgentHelper
构建您的备份代理。与扩展 BackupAgent
相比,使用 BackupAgentHelper
构建备份代理所需的代码要少得多,因为您无需实现 onBackup()
和 onRestore()
。
您的 BackupAgentHelper
实现必须使用一个或多个备份帮助程序。备份帮助程序是一种由 BackupAgentHelper
调用以对特定类型的数据执行备份和恢复操作的专用组件。Android 框架目前提供两种不同的帮助程序:
SharedPreferencesBackupHelper
,用于备份SharedPreferences
文件。FileBackupHelper
,用于备份内部存储空间中的文件。
您可以在 BackupAgentHelper
中添加多个帮助程序,但每种数据类型只需要一个帮助程序。也就是说,如果您有多个 SharedPreferences
文件,则只需要一个 SharedPreferencesBackupHelper
。
对于您要添加到 BackupAgentHelper
中的每个帮助程序,您必须在 onCreate()
方法中执行以下操作:
- 实例化所需帮助程序类的一个实例。在类构造函数中,您必须指定要备份的文件。
- 调用
addHelper()
以将帮助程序添加到您的BackupAgentHelper
中。
以下各部分介绍了如何使用每个可用的帮助程序创建备份代理。
备份 SharedPreferences
当您实例化 SharedPreferencesBackupHelper
时,必须包含一个或多个 SharedPreferences
文件的名称。
例如,如需备份名为 user_preferences
的 SharedPreferences
文件,使用 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()
写入文件的位置相同)的一个或多个文件的名称。
例如,如需备份名为 scores
和 stats
的两个文件,使用 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()
- 备份管理器会在您请求备份后调用该方法。在该方法中,您会从设备读取应用数据,并将要备份的数据传递到备份管理器,如执行备份部分所述。
onRestore()
备份管理器会在恢复操作执行期间调用该方法。该方法会传递您的备份数据,而通过备份数据,您的应用可以恢复之前的状态,如执行恢复部分所述。
当用户重新安装您的应用时,系统会调用该方法来恢复备份数据,但您的应用也可以请求恢复。
执行备份
备份请求不会使备份管理器立即调用您的 onBackup()
方法。相反,备份管理器会等待适当时机,然后为自上次执行备份以来请求备份的所有应用执行备份操作。此时,您必须将应用数据提供给备份管理器,以便它可以保存到云端存储空间。
只有备份管理器可以调用备份代理的 onBackup()
方法。每当您的应用数据发生变化且您希望进行备份时,您都必须通过调用 dataChanged()
来请求备份操作。如需了解详情,请参阅请求备份。
提示:在开发应用时,您可以利用 bmgr
工具通过备份管理器立即启动备份操作。
当备份管理器调用您的 onBackup()
方法时,它会传递以下三个参数:
oldState
- 一个开放的只读
ParcelFileDescriptor
,指向您的应用提供的上次备份状态。这不是云端存储空间中的备份数据,而是上次调用onBackup()
时备份的数据的本地表示形式,由newState
或通过onRestore()
定义。onRestore()
会在下一部分进行介绍。由于onBackup()
不允许您读取云端存储空间中的现有备份数据,因此您可以使用此本地表示形式来确定自上次备份以来您的数据是否发生过变化。 data
- 一个
BackupDataOutput
对象,用于将备份数据传递给备份管理器。 newState
- 一个开放的读/写
ParcelFileDescriptor
,会指向某个文件。在该文件中,您必须写入传递给data
的数据的表示形式。该表示形式可以像文件的上次修改时间戳一样简单。备份管理器下次调用您的onBackup()
方法时,系统会将该对象作为oldState
返回。如果您没有将备份数据写入newState
,则oldState
会在备份管理器下次调用onBackup()
时,指向一个空文件。
使用这些参数时,请实现 onBackup()
方法来执行以下操作:
通过将
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 步。
如果与
oldState
相比,您的数据发生了变化,请将当前数据写入data
,以将其备份到云端存储空间。您必须将每个数据区块以实体的形式写入
BackupDataOutput
。实体是由唯一键字符串标识的扁平化二进制文件数据记录。因此,从概念上来讲,您备份的数据集就是一组键值对。如需将一个实体添加到您的备份数据集,您必须执行以下操作:
调用
writeEntityHeader()
,为您要写入的数据和数据大小传递一个唯一的字符串键。调用
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);
对您要备份的每段数据执行此过程。如何将您的数据划分为多个实体由您决定,您甚至可以只使用一个实体。
无论您是否执行了备份(在第 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()
,以遍历数据集中的所有实体。对于找到的每个实体,请执行以下操作:
- 使用
getKey()
获取实体键。 将实体键与您应该已在
BackupAgent
类中声明为静态最终字符串的已知键值对列表进行比较。如果键与某个已知键字符串匹配,请输入一个语句来提取相应实体数据并将其保存到设备:- 使用
getDataSize()
获取实体数据大小,并创建该大小的字节数组。 - 调用
readEntityData()
并向其传递该字节数组(数据将进入其中),然后指定起始偏移量和要读取的数据大小。 - 现在,您的字节数组已满。您可以随意读取数据并将其写入设备。
- 使用
读取数据并将其写回设备后,请将数据状态写入
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 会根据其隐私权政策将此类数据视为个人信息。
此外,用户还可以通过 Android 系统的备份设置停用数据备份功能。如果用户停用备份功能,Android Backup Service 会删除所有已保存的备份数据。用户可以在设备上重新启用备份功能,但 Android Backup Service 不会恢复以前删除的任何数据。