二十五岁时我们都一样愚蠢、多愁善感,喜欢故弄玄虚,可如果不那样的话,五十岁时也就不会如此明智。
标题:7.11 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,可能就会用此参数来选择备份数据。