原文
http://developer.android.com/guide/topics/data/backup.html
快速查看
· 将用户数据备份到云中心以防丢失。
· 如果用户升级到运行Android的新设备,程序可以恢复用户数据到新设备中。
· 可方便地用BackupAgentHelper备份SharedPreference和私有文件。
· 需要API Level 8支持。
在本文中
关键类
参阅
为了给应用程序的数据和配置信息提供数据还原点,Android的备份backup服务允许把需持久保存的数据拷贝到远程“云”存储中。如果用户恢复了出厂设置或者换用新的Android设备,系统将在再次安装应用程序时自动恢复备份数据。这样,就不需要用户复制之前的数据和程序配置信息。整个过程对于用户而言完全透明,不会影响程序的功能和用户体验度。
在备份过程中(应用程序可发起请求),Android的备份管理器(BackupManager)将查找应用程序中需备份的数据,并把数据交给备份传输器,传输器再把数据传送给云存储。在恢复时,备份管理器从备份传输器取回备份数据并将其返回给应用程序,然后应用程序就能把数据恢复到设备上。应用程序也能够发起恢复请求,但不是必须的——如果程序安装完毕且存在用户相关的备份数据,Android会自动执行恢复操作。恢复备份数据主要发生于以下场合:用户重置设备或者升级到新设备后,以前装过的应用程序又被再次安装。
注意:备份服务并不是为以下用途设计的:与其它客户端同步、在程序正常生命周期内保存数据。备份数据不允许随意读写,除通过备份管理器提供的API外无法访问数据。
备份传输器是Android备份框架的客户端组件,它可由设备制造商和提供商定制。备份传输器可以因设备不同而不同,对于应用程序而言它是透明的。备份管理器的API将应用程序和实际备份传输器联接起来——程序通过一组固定的API与备份管理器进行通讯,而不必关心底层的传输过程。
并不是所有Android平台的设备都能支持数据备份。不过,即使设备不支持备份传输,对程序运行也不会有什么影响。如果确信用户将受益于数据备份服务,只管按照本文所述去实现、测试并发布即可,而不必去关心哪些设备会真正执行备份工作。就算是在不支持备份传输的设备上,程序仍然会正常运行,只是不能接收备份管理器的请求进行数据备份而已。
尽管对当前所传输内容一无所知,但尽管放心,备份数据是不能被设备上的其它程序读取的。在备份过程中,只有备份管理器和备份传输器有权限访问被提交的数据。
警告:因为云存储和传输服务依设备而各不相同,Android不保证使用备份服务数据的安全性。如果要利用备份服务保存敏感数据(比如用户名和密码),应该始终保持谨慎态度。
基本情况
为了备份应用程序数据,需要实现一个备份代理。此备份代理将被备份管理器调用,用于提供所需备份的数据。当程序重装时,还要调用此代理来恢复数据。备份管理器处理所有与云存储之间的数据传输工作(利用备份传输器),备份代理则负责所有对设备上数据的处理。
要实现备份代理,必须:
1. 在manifest文件内用android:backupAgent属性声明备份代理。
2. 用备份服务对应用程序进行注册。Google为大多数Android平台的设备提供了Android备份服务 ,必须对应用程序进行注册以便服务生效。为了在它们的服务器上存储数据,其它所有的备份服务提供方也都可能需要注册。
3. 用以下两种方式之一进行备份代理的定义:
BackupAgent类提供了核心接口,程序通过这些接口与备份管理器进行通讯。如果直接继承此类,必须覆盖onBackup()和onRestore()方法来处理数据的备份和恢复操作。
BackupAgentHelper类提供了BackupAgent类的易用性封装,它减少了需编写的代码数量。在BackupAgentHelper内,必须用一个或多个“helper”对象来自动备份和恢复特定类型的数据,因此不再需要实现onBackup()和onRestore()方法了。
Android目前提供两种backup helper,用于从SharedPreferences和internal storage备份和恢复整个的文件。
在Manifest中声明备份代理
这是最容易的一步,一旦确定了类名,就可在manifest的<application>标签内用android:backupAgent属性声明备份代理了。
例如:
<manifest ...>
...
<application android:label="MyApplication"
android:backupAgent="MyBackupAgent">
<activity ... >
...
</activity>
</application>
</manifest>
其它可能会用到的属性是android:restoreAnyVersion。这个属性用布尔值标明恢复数据时是否忽略当前程序和产生备份数据的程序之间的版本差异(默认值是“false”)。详情请参阅检查恢复数据的版本。
注意:备份服务和API只在运行API Level 8(Android 2.2)以上版本的设备上才可用,因此应把android:minSdkVersion属性设为“8”。当然,如果程序实现了良好的向后兼容性,可以仅针对API Level 8以上版本的设备提供备份功能,而对其它旧版本设备则保持兼容即可。
为Android备份服务进行注册
Google为大多数Android 2.2以上版本的设备提供了利用Android备份服务进行的备份传输服务。
为了程序能利用Android备份服务执行备份操作,必须对程序进行注册以获得一个Backup Service Key,然后在Androidmanifest文件中声明这个Key。
要获取Backup Service Key,请为Android服务进行注册。注册时会得到一个Backup ServiceKey和Android manifest文件内相应的<meta-data> XML代码,这段代码必须包含在<application> 元素下。例如:
<applicationandroid:label="MyApplication"
android:backupAgent="MyBackupAgent">
...
<meta-data android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAIDaYEVgU6DJnyJdBmU7KLH3kszDXLv_4DIsEIyQ"/>
</application>
android:name必须是"com.google.android.backup.api_key",android:value也必须是注册Android备份服务时收到的Backup Service Key。
如果存在多个应用程序,必须根据各自的程序包名称(package name)为每一个程序进行注册。
注意:即使设备能够支持,Android备份服务提供的备份传输器也不一定在所有Android 平台的设备上都能执行。有些设备可能使用不同的传输器来为备份提供支持,有些设备可能根本就不支持备份,程序是无法知道设备使用何种传输器的。不过,假如为程序实现了备份,就必须为备份服务指定Backup Service Key,这样设备利用Android备份服务进行传输时程序就能顺利执行备份工作。如果设备不使用Android备份服务,带Backup ServiceKey的<meta-data>元素将被忽略。
继承BackupAgent
大多数应用程序应该不需要直接继承使用BackupAgent 类,取而代之的是继承BackupAgentHelper类,并利用BackupAgentHelper内建的helper类自动备份和恢复文件。不过,如果需要实现以下目标的话,也许希望能直接继承BackupAgent:
· 将数据格式版本化。例如需要在恢复数据时修正格式,可以建立一个备份代理,在数据恢复过程中如果发现当前版本和备份时的版本不一致,可以执行必要的兼容性修正工作。详情请参阅检查恢复数据的版本。
· 不是备份整个文件,而是指定备份部分数据及指定恢复各部分数据。(这也有助于管理不同版本的数据,因为是把数据作为唯一Entity来读写,而不是读写整个文件。)
· 备份数据库中的数据。如果用到SQLite数据库并且希望用户重装系统时能恢复其中数据,需要建立自定义的BackupAgent。它在备份时读取库中数据,而在恢复时建表并插入数据。
如果不需要执行以上的任务,而只是从SharedPreferences或内部存储备份完整的文件,请跳转到继承BackupAgentHelper。
必需的方法
通过继承BackupAgent创建备份代理时,必须实现以下回调方法:
备份管理器在程序请求备份后将调用本方法。如下文执行备份所述,在本方法中实现从设备读取应用程序数据,并把需备份的数据传递给备份管理器。
备份管理器在恢复数据时调用本方法(也可以主动请求恢复,但在用户重装应用程序时系统会自动执行数据恢复。)如下文执行恢复所述,备份管理器调用本方法时将传入备份的数据,然后就可把数据恢复到设备上。
执行备份
备份应用程序数据时,备份管理器将调用onBackup()方法。在此方法内必须把数据提供给备份管理器,然后数据被保存到云存储中。
只有备份管理器能够调用备份代理中的onBackup()方法。每当数据发生改变并需要执行备份时,必须调用dataChanged()发起一次备份请求(详情请参阅请求备份)。备份请求并不会立即导致onBackup()方法的调用。备份服务器会等待合适的时机,为上次备份操作后又发出备份请求的所有应用程序执行备份。
提示:在开发应用程序的过程中,可以用bmgr工具让备份管理器立即执行备份操作。
当备份管理器调用onBackup()方法时,传入以下三个参数:
oldState
已打开的、只读的文件描述符ParcelFileDescriptor,指向应用程序提供的有关上次备份数据状态的文件。这不是来自云存储的备份数据,而是记录上次调用onBackup()所备份数据相关状态信息的本地文件(如下文newState所定义,或来自下节onRestore())。因为onBackup()不允许读取保存于云存储的数据,可以根据此信息来判断数据自上次备份以来是否变动过。
data
BackupDataOutput对象,用于将备份数据传给备份管理器。
newState
已打开的、可读写的文件描述符ParcelFileDescriptor,指向一个文件,必须将提交给data参数的数据相关状态信息写入此文件(此状态信息可以简单到只是文件的最后修改时间戳)。备份管理器下次调用onBackup()时,本对象作为oldState传入。如果没有往newState写入信息,则备份管理器下次调用onBackup()时oldState将指向一个空文件。
利用以上参数,可以实现onBackup()方法如下:
1.通过比较oldState,检查自上次备份以来数据是否发生改变。从oldState读取信息的方式取决于当时写入的方式(见第3步)。最简单的记录文件状态的方式是写入文件的最后修改时间戳。以下是如何从oldState读取并比较时间戳的例子:
//获取oldState输入流
FileInputStream instream= new FileInputStream(oldState.getFileDescriptor());
DataInputStreamin = new DataInputStream(instream);
try{
// 从state文件和数据文件获取最后修改时间戳
longstateModified = in.readLong();
longfileModified = mDataFile.lastModified();
if (stateModified!=fileModified){
// The file has been modified, so do abackup
// Or the time on the device changed, so besafe and do a backup
} else {
// Don't back up because the file hasn'tchanged
return;
}
}catch (IOException e){
// Unable to read state file... be safe anddo a backup
}
如果数据没有发生变化,就不需要进行备份,请跳转到第3步。
2.在和oldState比较后,如果数据发生了变化,则把当前数据写入data以便将其返回并上传到云存储中去。
必须以BackupDataOutput中的“entity”方式写入每一块数据。一个entity是用一个唯一字符串键值标识的拼接二进制数据记录。因此,所备份的数据集其实上是一组键值对。
要在备份数据集中增加一个entity,必须:
1. 调用writeEntityHeader(),传入代表写入数据的唯一字符串键值和数据大小。
2. 调用[url=http://developer.android.com/reference/android/app/backup/BackupDataOutput.html#writeEntityData(byte[], int)]writeEntityData()[/url],传入存放数据的字节类型缓冲区,以及需从缓冲区写入的字节数(必须与传给writeEntityHeader()的数据大小一致)。
例如,以下代码把一些数据拼接为字节流并写入一个entity:
//为数据创建缓冲区流和输出流
ByteArrayOutputStream bufStream= new ByteArrayOutputStream();
DataOutputStream outWriter= new DataOutputStream(bufStream);
//写入结构化的数据
outWriter.writeUTF(mPlayerName);
outWriter.writeInt(mPlayerScore);
//通过BackupDataOutput发送数据到备份管理器Backup Manager
byte[] buffer=bufStream.toByteArray();
int len= buffer.length;
data.writeEntityHeader(TOPSCORE_BACKUP_KEY,len);
data.writeEntityData(buffer,len);
对每一块需备份的数据都要执行以上操作。程序负责把数据切分为多个entity(当然也可以只用一个entity)。
3.无论是否执行备份(第2步),都要把当前数据的状态信息写入newState ParcelFileDescriptor指向的文件内。备份管理器会在本地保持此对象,以代表当前备份数据。下次调用onBackup()时,此对象作为oldState返回给应用程序,由此可以决定是否需要再做一次备份(如第1步所述)。如果不把当前数据的状态写入此文件,下次调用时oldState将返回空值。
以下例子把文件最后修改时间戳作为当前数据的状态存入newState:
FileOutputStream outstream = newFileOutputStream(newState.getFileDescriptor());
DataOutputStream out = newDataOutputStream(outstream);
long modified = mDataFile.lastModified();
out.writeLong(modified);
警告:如果应用程序数据存放于文件中,请确保使用同步语句(synchronized)来访问文件。这样在应用程序的Activity写文件时,备份代理就不会去读文件了。
执行恢复
恢复程序数据时,备份管理器将调用备份代理的onRestore()方法。调用此方法时,备份管理器会把备份的数据传入,以供恢复到设备中去。
只有备份服务器能够调用onRestore(),在系统安装应用程序并且发现有备份数据存在时,调用会自动发生。不过,也可以通过调用requestRestore()来发起恢复数据的请求(详情参阅请求恢复)。
注: 在开发应用程序的过程中,可以用bmgr工具发起恢复数据的请求。
当备份管理器调用onRestore()方法时,传入以下三个参数:
data
BackupDataInput对象,用以读取备份数据。
appVersionCode
整数,表示备份数据时应用程序manifest中的android:versionCode属性。可以用于核对当前程序版本并确定数据格式的兼容性。关于利用此版本号来处理不同版本恢复数据的详细情况,请参阅下文检查恢复数据的版本。
newState
已打开的,可读写的文件描述符ParcelFileDescriptor,指向一个文件,这里必须写入最后一次提交data数据的备份状态。本对象在下次调用onBackup()时作为oldState返回。回想一下,onBackup()方法也必须写入newState 对象——这里也同样要这么做。这样即使设备重置后第一次调用onBackup(),也能确保有可用的oldState对象能传给onBackup()方法。
在实现onRestore()时,应该对data 调用readNextHeader(),以遍历数据集里所有的entity。对其中每个entity须进行以下操作:
1.用getKey()获取entity的键值。
2. 将此entity键值和已知键值清单进行比较,这个清单应该已经在BackupAgent继承类中作为字符串常量(static finalstring)进行定义。一旦键值匹配其中一个键,就执行读取entity数据并保存到设备的语句:
1. 用getDataSize()读取entity数据大小并据其创建字节数组。
2.调用[url=http://developer.android.com/reference/android/app/backup/BackupDataInput.html#readEntityData(byte[], int, int)]readEntityData()[/url],传入字节数组作为获取数据的缓冲区,并指定起始位置和读取字节数。
3.字节数组将被填入数据,按需读取数据并写入设备即可。
3. 把数据读出并写回设备以后,和上面onBackup()过程类似,把数据的状态写入newState参数。
下面是把前一节例子中所备份的数据进行恢复的示例:
@Override
publicvoid onRestore(BackupDataInput data,int appVersionCode,
ParcelFileDescriptor newState)throws IOException{
// 应该是只有一个entity,
// 但最安全的方法还是用循环来处理
while (data.readNextHeader()){
String key = data.getKey();
int dataSize = data.getDataSize();
// 如果键值是所需的(保存Top Score),注意这个键值是用于
// 写入备份entity header
if (TOPSCORE_BACKUP_KEY.equals(key)){
// 为BackupDataInput创建输入流
byte[] dataBuf = new byte[dataSize];
data.readEntityData(dataBuf,0, dataSize);
ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
DataInputStream in = new DataInputStream(baStream);
// 从备份数据中读取player name和score
mPlayerName= in.readUTF();
mPlayerScore= in.readInt();
// Record the score on the device (to afile or something)
recordScore(mPlayerName,mPlayerScore);
} else {
// 不知道这个entity键值,跳过,(这本不该发生)
data.skipEntityData();
}
}
// Finally, write to the state blob (newState) thatdescribes the restored data
FileOutputStreamoutstream = new FileOutputStream(newState.getFileDescriptor());
DataOutputStream out = new DataOutputStream(outstream);
out.writeUTF(mPlayerName);
out.writeInt(mPlayerScore);
}
在以上例子中,传给onRestore()的appVersionCode参数没有被用到。假如用户程序的版本已经降级(比如从1.5降到1.0),可能就会用此参数来选择备份数据。更多信息请参阅检查恢复数据的版本。
关于BackupAgent的完整例子,请参阅例程备份和恢复中的ExampleAgent类。
继承BackupAgentHelper
如果要备份整个文件(来自SharedPreferences或内部存储),应该用BackupAgentHelper创建备份代理来实现。因为不必实现onBackup()和onRestore()了,用BackupAgentHelper创建备份代理所需的代码量将远远少于BackupAgent。
BackupAgentHelper的实现必须要使用一个或多个backup helper。backup helper是一种专用组件,BackupAgentHelper用它来对特定类型的数据执行备份和恢复操作。Android框架目前提供两种helpers:
· SharedPreferencesBackupHelper用于备份SharedPreferences文件。
· FileBackupHelper 用于备份来自内部存储的文件。
在BackupAgentHelper中可包含多个helper,但对于每种数据类型只需用到一个helper 。也就是说,即使存在多个SharedPreferences文件,也只需要一个SharedPreferencesBackupHelper。
对于每个要加入BackupAgentHelper的helper,必须在onCreate()中执行以下步骤:
1. 实例化所需的helper。在其构造方法里必须指定需备份的文件。
2. 调用addHelper() 把helper加入BackupAgentHelper。
下一节描述了如何使用每种helper创建备份代理。
备份SharedPreferences
实例化SharedPreferencesBackupHelper时,必须包括一个或多个SharedPreferences 文件。
例如,假设需备份的SharedPreferences文件名为“user_preferences”,完整的使用BackupAgentHelper的备份代理代码类似如下:
publicclass MyPrefsBackupAgentextends BackupAgentHelper{
// SharedPreferences 文件名
static final String PREFS = "user_preferences";
// 唯一标识备份数据的键值
static final String PREFS_BACKUP_KEY = "prefs";
// 申请helper并加入备份代理
@Override
public void onCreate(){
SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this,PREFS);
addHelper(PREFS_BACKUP_KEY,helper);
}
}
好,这就是一个备份代理的完整实现。SharedPreferencesBackupHelper内含了备份和恢复SharedPreferences文件的所有代码。
当备份管理器调用onBackup()和onRestore()时,BackupAgentHelper调用helper来对给定文件执行备份和恢复操作。
注:SharedPreferences是线程安全的,因此可以从备份代理和其它activity中安全地读写shared preferences文件。
备份其它文件
在实例化FileBackupHelper时,必须包含一个或多个保存于程序内部存储中的文件名称。(路径的描述方式类似getFilesDir(),并且作为openFileOutput()写入文件的路径。)
比如,需要备份两个名为“scores”和“stats”的文件,备份代理使用BackupAgentHelper示例如下:
publicclass MyFileBackupAgentextends BackupAgentHelper{
// SharedPreferences文件的名称
static final String TOP_SCORES = "scores";
static final String PLAYER_STATS = "stats";
// 唯一标识备份数据集的键值
static final String FILES_BACKUP_KEY = "myfiles";
// 申请helper并加入备份代理
voidonCreate() {
FileBackupHelper helper = new FileBackupHelper(this,TOP_SCORES, PLAYER_STATS);
addHelper(FILES_BACKUP_KEY,helper);
}
}
FileBackupHelper包含了备份和恢复存于内部存储的文件所需的全部代码。
但是,读写内部存储文件不是线程安全的。要确保activity操作文件的时候备份代理不会去读写文件,每次读写文件时必须使用同步语句。比如,Activity读写文件时,需要用一个对象作为同步语句的内部锁。
//内部锁对象
staticfinal Object[]sDataLock= new Object[0];
有趣的事实:长度为零的数组要比普通对象更轻量化,因此用作内部锁会是个好主意。
然后,每次读写文件时用这个锁创建同步语句。以下是把游戏分数写入文件的同步语句示例:
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,"Unableto write to file");
}
应该用同一个锁同步读取文件的语句。
然后,在BackupAgentHelper内,必须覆盖onBackup()和onRestore()方法,用同一个内部锁同步备份和恢复操作。比如,上例中MyFileBackupAgent需要以下方法:
@Override
publicvoid onBackup(ParcelFileDescriptor oldState,BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
// Hold the lock while the FileBackupHelper performsbackup
synchronized (MyActivity.sDataLock){
super.onBackup(oldState,data,newState);
}
}
@Override
publicvoid onRestore(BackupDataInput data,int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
// Hold the lock while the FileBackupHelper restores thefile
synchronized (MyActivity.sDataLock){
super.onRestore(data,appVersionCode, newState);
}
}
好了,所有要做的工作仅仅是在onCreate()方法内加入FileBackupHelper,覆盖onBackup()和onRestore()并同步读写。
关于用FileBackupHelper实现BackupAgentHelper的例子,请参阅例程备份和恢复中的FileHelperExampleAgent类。
检查恢复数据的版本
在把数据保存到云存储中去时,备份管理器会自动包含应用程序的版本号,版本号是在manifest文件的android:versionCode 属性中定义的。在调用备份代理恢复数据之前,备份管理器会查询已安装程序的android:versionCode,并与记录在备份数据中的版本号相比较。如果备份数据的版本比设备上的要新,则意味着用户安装了旧版本的程序。这时备份管理器将停止恢复操作,onRestore()方法也不会被调用,因为把数据恢复给旧版本的程序是没有意义的。
用android:restoreAnyVersion属性可以取代以上规则。此属性用“true”或“false”标明是否在恢复时忽略数据集的版本,默认值是“false”。如果将其设为“true”,备份管理器将忽略android:versionCode并且每次都会调用onRestore()方法。这时候可以在onRestore()里人工检查版本,并在版本冲突时采取必要的措施保证数据的兼容性。
为了便于在恢复数据时对版本号进行判断处理,onRestore()把备份数据的版本号作为appVersionCode 参数和数据一起传入方法。而用PackageInfo.versionCode可以查询当前应用程序的版本号,例如:
PackageInfo info;
try{
Stringname = getPackageName();
info = getPackageManager().getPackageInfo(name,0);
}catch (NameNotFoundException nnfe){
info = null;
}
int version;
if(info!= null) {
version =info.versionCode;
}
然后,简单比较一下PackageInfo中的version 和传入onRestore()的appVersionCode即可。
警告:请确认已经理解了android:restoreAnyVersion设为“true”的后果。如果不是所有版本的程序都能在onRestore()时正确解析数据格式的差异,那么保存到设备上的数据格式可能会与已安装的版本不兼容。
请求备份
任何时候都可以通过调用dataChanged()来发起备份请求。此方法通知备份管理器用备份代理来备份数据。然后,备份管理器将会适时调用备份代理的onBackup()方法。通常每次数据发生变化时都应该请求备份数据(比如用户修改了需保存的程序配置)。如果在备份管理器实际执行前连续调用了dataChanged()很多次,代理仅会执行一次onBackup()。
注: 在程序开发过程中,可以用bmgr tool发起备份请求,备份将会立即执行。
请求恢复
在程序正常的生命周期内,应该不需要发起恢复数据的请求。在程序安装完成时,系统会自动检查备份数据并执行恢复操作。不过必要时,也可以通过调用requestRestore()来人工发起恢复数据的请求。这时,备份管理器会调用onRestore(),并把现有备份数据集作为数据传入该方法。
注:在程序开发过程中,可以用bmgr tool发起恢复数据的请求。
测试备份代理
一旦实现了备份代理,就可以用bmgr按以下步骤测试备份和恢复功能了:
1.在合适的Android系统镜像上安装应用程序
o 如果使用仿真器,须创建和使用Android 2.2(API Level 8)以上版本的AVD。
o 如果使用硬件设备,则此设备必须运行Android 2.2以上版本并内置Android Market功能。
2.确保启用备份功能
o 如果使用仿真器,可以在SDK tools/路径下用以下命令启用备份功能:
adbshell bmgr enable true
o 如果使用硬件设备,则在系统Settings, 选择Privacy,启用Back up my data 和Automaticrestore。
3. 运行程序并初始化一些数据。
如果程序已经正确地实现了备份代码,那每次数据变化时都会请求备份。例如,每当用户改变了一些数据,程序将调用dataChanged(),这就往备份管理器的请求队列里加入了一个备份请求。出于测试的目的,也可以用以下bmgr命令发起一个请求:
adbshell bmgr backupyour.package.name
4. 执行备份操作:
adbshell bmgr run
这一步强迫备份管理器执行所有已入队列的备份请求。
5. 卸载应用程序:
adbuninstallyour.package.name
6. 重新安装应用程序。
如果备份代理成功运行,那第4步里备份的数据将会被恢复。
补充
文章精选