Android BackupAgent实现数据备份与恢复

大多数应用程序不需要直接继承 BackupAgent 类,而是继承 BackupAgentHelper 类,并利用 BackupAgentHelper 内建的 helper 类自动备份和恢复文件。

不过,以下三种情况需要直接继承 BackupAgent 类来实现备份代理:
  • 将数据格式版本化。例如,需要在恢复数据时修正格式,可以建立一个备份代理,在数据恢复过程中,如果发现当前版本和备份时的版本不一致,可以执行必要的兼容性修正工作。
  • 不是备份整个文件,而是指定备份部分数据以及恢复部分数据到设备。
  • 备份数据库中的数据。若应用程序使用了 SQLite 数据库并且希望当用户重装应用程序时能够恢复数据库中的数据,则需要建立一个自定义的 BackupAgent。它在备份操作时从数据库中读取合适的数据,在恢复数据操作时建立数据表并插入数据。

通过继承 BackupAgent 类创建备份代理时,必须实现以下两个方法:
  • onBackup():备份管理器在程序请求进行备份操作后将调用该方法。在该方法中实现从设备读取应用程序数据,并把需备份的数据传递给备份管理器的操作。
  • onRestore():备份管理器在恢复数据时调用该方法。备份管理器调用该方法时将传入备份的数据,并通过该方法将数据恢复到设备上。

1)备份数据

应用程序发出数据备份请求时,备份管理器将调用 onBackup() 方法。在此方法内必须把要备份的数据提供给备份管理器,然后将数据保存到云存储中。

只有备份管理器能够调用备份代理中的 onBackup() 方法。当数据发生改变并需要执行备份时,需要调用 dataChanged() 方法发起备份请求。

备份请求并不会立即导致 onBackup() 方法的调用,备份服务器会等待合适的时机,为上次备份操作后又发出备份请求的所有应用程序执行备份操作。

onBackup() 方法需要传入三个参数,所代表的意义分别如下。

名称 作用
oldState 表示已打开的、只读的文件描述符 ParcelFileDescriptor,指向应用程序提供的上次备份数据状态的文件。该文件不是来自云存储的备份数据,而是记录上次调用 onBackup() 备份数据相关状态信息的本地文件。
onBackup() 不能读取保存于云存储的数据,可以根据此信息来判断数据自上次备份以来是否变动过。
Data BackupDataOutput 对象,用于将要备份的数据传给备份管理器。
newState 表示已打开的、可读写的文件描述符 ParcelFileDescriptor,指向一个用于将提交给 data 参数的数据相关状态信息写入的文件,状态信息可以简单到只是文件的最后修改时间。
备份管理器下次调用 onBackup() 时,该对象作为 oldState 传入。若没有向 newState 写入信息,则备份管理器下次调用 onBackup() 时 oldState 将指向一个空文件。

利用以上参数可以实现 onBackup(),方法如下:

1)通过比较 oldState,检查自上次备份以来数据是否发生过改变。从 oldState 读取信息的方式取决于当时写入的方式(见步骤3)。

最简单的记录文件状态的方式是写入文件的最后修改时间戳。以下是从 oldState 读取并比较时间戳的代码:
// 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 = mDataFile.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 中的“entity“方式写入每一块数据。

一个 entity 是一个二进制数据记录,使用一个唯一的字符串键值进行标识。因此,所备份的数据集实际上是一组键值对。要在备份数据集中增加一个 entity,必须:
  • 调用 writeEntityHeader() 方法,传入代表要写入数据的唯一字符串键值和数据大小。
  • 调用 writeEntityData() 方法,传入存放着数据的字节缓冲区,以及需从缓冲区写入的字节数,该字节数应该与传给 writeEntityHeader() 的数据大小一致。

下面的示例代码演示了把一些数据拼接为字节流并写入一个 entity 的过程:
// Create buffer stream and data output stream for our data
ByteArrayOutputStream bufStream=new ByteArrayOutputStream();
DataOutputStream outWriter=new DataOutputStream(bufStream);
// Write structured data
outWriter.writeUTF(mPlayerName);
outWriter.writeInt(mPlayerScore);
// 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);
需要备份的每一块数据都要执行一次该操作。如何将数据切分为 entity 由开发者决定。

3)无论是否执行了数据备份(步骤2),都要把当前数据的状态信息写入 newState ParcelFileDescriptor 指向的文件内。备份管理器会在本地保持此对象,以代表当前备份数据。

下次调用 onBackup() 时,此对象作为 oldState 返回给应用程序,由此可以决定是否需要再做一次备份(如步骤1所述)。如果不把当前数据的状态写入此文件,下次调用时 oldState 将返回空值。

以下示例代码把文件的最后修改时间戳作为当前数据的状态存入 newState:
FileOutputStream outstream=new FileOutputStream(newState.getFileDescriptor());
DataOutputStream out=new DataOutputStream(outstream);
long modified=mDataFile.lastModified();
out.writeLong(modified);
需要注意的是,如果应用程序数据存放于文件中,需要使用同步语句(synchronized)来访问文件。这样在应用程序的 Activity 进行写文件操作时,备份代理就不会去读文件了。

执行数据恢复操作

恢复程序数据时,备份管理器将调用备份代理的 onRestore() 方法。调用此方法时,备份管理器会把在云存储备份的数据传入,以供恢复到设备中去。

只有备份服务器能够调用 onRestore() 方法,在系统安装应用程序并且发现有备份数据存在时,数据恢复操作会自动发生。此外,应用程序也可以通过调用 requestRestore() 方法来发起恢复数据的请求。

当备份管理器调用 onRestore() 方法时,传入以下三个参数。
  • Data:BackupDataInput 对象,用以读取备份数据。
  • appVersionCode:整型数据,表示备份数据时,应用程序的 manifest 的 android:versionCode 属性。可以用于核对当前应用程序版本并确定数据格式的兼容性。
  • newState:已打开的、可读写的文件描述符 ParcelFileDescriptor,指向一个文件,用于写入最后一次提交 data 数据的备份状态。本对象在下次调用 onBackup() 方法时作为 oldState 返回。

在实现 onRestore() 时,应该对 data 调用 readNextHeader(),以遍历数据集里所有的 entity。对其中每个 entity 需进行以下操作:

1)用 getKey() 方法获取 entity 的键值。

2)将此 entity 键值和已知键值清单进行比较,这个清单应该已经在 BackupAgent 继承类中作为字符串常量定义。一旦键值匹配其中一个键,就执行读取 entity 数据并保存到设备的操作:
  • 用 getDataSize() 读取 entity 数据大小并据其创建字节数组。
  • 调用 readEntityData(),传入字节数组作为获取数据的缓冲区,并指定起始位置和读取字节数。
  • 字节数组将被填入数据,按需读取数据并写入设备即可。

3)把数据读出并写回设备以后,把数据的状态写入 newState 参数。

下面的实例代码将前面的例子中所备份的数据进行了恢复:
@Override
public void onRestore (BackupDatalnput 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 BackupDatalnput
            byte[] dataBuf=new byte[dataSize];
            data.readEntityData (dataBuf, 0, dataSize) ;
            ByteArraylnputStream baStream=new ByteArraylnputStream (dataBuf) ; DatalnputStream in=new DatalnputStream (baStream) ;

            // Read the player name and score from the backup data
            mPlayerName=in.readUTF(); mPlayerScore=in.readlnt();

            // Record the score on the device (to a file or something)
            recordScore (mPlayerName, mPlayerScore) ;
        } 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 (mPlayerName);
    out.writelnt (mPlayerScore);
}
在以上代码中,传给 onRestore() 的 appVersionCode 参数没有被用到。如果用户程序的版本降低,比如从 1.5 降到 1.0,可能就会用此参数来选择备份数据。