概述
这些数据耗费了我们很多时间和精力,对我们而言极为重要。如果我们的设备换代了或者重新安装了某个应用,之前使用的数据如果能自动保留,那将是非常出色的用户体验。而保留数据的第一步则在于Backup环节。
2.2 数据来源
用户的数据可以笼统地划分为三块:登录账号相关的身份数据、系统设置相关的偏好以及各App的数据。这三块数据的类型不同、位置不同,进而导致Backup的实现也不同。
- App数据:应用内部的图片,视频等数据。这是我们尤为关心的数据,如何安全完整地转移这些数据是Backup功能的目标所在,也是本文需要讲解的核心内容
- 身份数据:用户登录的身份数据。可以通过Smart Lock或Account Transfer API在设备间立即恢复登录状态
- 设置偏好:系统设置App和SettingProvider将记录用户的偏好数据,甚至包括用户授予App的权限记录。系统将针对这些设置数据备份和恢复
2.3 备份对象
我们知道可以将数据存放在App目录,也可以存放于公共目录。但随着Android系统针对公共目录的限制愈加严格,将数据存放到App自己的目录显得更加合理。
App自身目录的这块数据顺理成章地成为Backup功能的主要对象,按照文件的类型可以细分如下。
类型 | 路径 | 取得对应文件的API |
---|---|---|
data | /data/data/com.xxx/ | getDataDir()/getDir() |
files | /data/data/com.xxx/files/ | getFilesDir() |
databases | /data/data/com.xxx/databases/ | getDatabasePath() |
sharedpreferences | /data/data/com.xxx/sp/ | getSharedPreferences() |
注意:
- 放置在外部存储空间中的文件也是支持的,这里不再赘述
- cache、nobackup等目录下的文件不在Backup对象内
Backup操作从最外层的data目录开始,按照文件单位逐个读取逐个备份。目录内的文件一般按照文件名的顺序进行备份,但这个顺序无法保证,取决于File#list()
API的结果。
graph TD
data --> files --> databases --> sharedpreferences
2.4 如何开启
在Manifest文件里使用allowBackup
属性可以控制Backup功能的开关。这个属性早在Android 1.6(API 4)的时候便引入了,起初默认是关闭的。
allowBackup Added in API level 4 Whether to allow the application to participate in the backup and restore infrastructure. If this attribute is set to false, no >backup or restore of the application will ever be performed, even by a full-system backup that would otherwise cause all >application data to be saved via adb. The default value of this attribute is true.
可能是Android设备火了,备份恢复的需求越来越大,自Android 6.0之后默认值变成了true,即默认将支持Backup功能。
2.5 开启的隐患
allowBackup
属性开启的话,开发者使用几个简单的adb命令就可以将应用的数据备份和恢复。意味着自己的数据被转移到别人的设备上是非常简单的,这无疑造成了具大的安全隐患。
后面的实战将会揭晓控制备份和恢复的诸多可能,比如在恢复的逻辑里加入了限制使得恶意的恢复失败。但即便在恢复阶段拦截了,备份的ab文件仍掌握在别人手里,仍不稳妥。因为通过破解备份文件也可以阅读到部分甚至全部内容。
所以涉及到私密数据的App最好将该属性关闭,自行考虑数据的同步方式,比如通过账号系统恢复。
这个属性曾经引发安全隐患,详情可参考:
blog.csdn.net/zihao2012/a…
2.6 备份模式
Android 6.0之前Backup功能只有键值对备份
(Key-value Backup)这一种模式,而且默认是关闭的。
想要打开键值对备份功能得将allowBackup属性设置为true,并指定BackupAgent实现,即明确地告知诉Backup功能每个文件按照什么key备份到Android Backup Service
。简单来讲,必须给Backup功能提供一个备份文件的映射关系,好让它知道备份的源头和恢复的目标。
6.0之后allowBackup属性默认为true,但打开的不是键值对备份,而是新引入的自动备份
(Auto Backup)。自动备份模式执行傻瓜式的全体备份和恢复,可供备份文件存放的空间更大,便捷够用更推荐。。
两个模式在备份的频次、文件的存放位置、恢复的执行时机等细节都很不一样。
备份模式 | 键值对备份 | 自动备份 |
---|---|---|
支持版本 | Android 2.2 | Android 6.0 |
开关办法 | 默认关闭,需手动开启allowBackup并指定BackupAgent | 默认开启,关闭需要将allowBackup置为false |
备份定制 | BackupAgent里指定备份和恢复的文件 | 可以通过XML配置备份和不备份的文件列表,也可以通过BackupAgent改写备份恢复的逻辑,定制性高 |
备份时机 | 需要App调用API手动发起备份 | 自动进行,大约每天一次 |
备份的托管位置 | Android Backup Service/Google服务器 | Google Drive云盘 |
备份限制 | 上限只有5M | 上限有25M |
恢复时机 | APK安装的时候自动恢复,也可以调用API手动发起恢复 | APK安装的时候自动恢复 |
原理细节 | 回调到BackupAgent的onBackup()和onRestore() | 回调到BackupAgent的onFullBackup()和onRestoreFile() |
2.7 备份的托管位置
使用键值对模式备份的文件托管在Google服务器
(Android Backup Service),Google承诺将会加密传输这些数据,并尊重隐私条款。同时在App关闭Backup功能的时候删除这些备份,可以放心使用。
如果采用了默认的自动备份模式,那数据存放在Google Drive云盘
。云盘拥有自己的账号系统,使用的是账号级别的加密保护,更为安全。
2.8 实现原理
那Android系统是如何实现Backup和Restore功能的呢?
在解答这个问题之前,我们先思考下如果你是Google开发者,你会怎么实现?
这里有个ContentProvider
方案。简言之,使用一个统一调度的App通过ContentProvider组件向各个实现了特定Uri或Permission的ContentProvider App发出读写数据的请求。
- 各App通过ContentProvider将需要备份的Data、File、DB以及SP文件传输出去,调度App收集到包名为单位的备份文件集合,加密后上传到服务器或内存卡
- 恢复则是调度App将文件解密之后通过ContentProvider再回传给各App,各App自行执行恢复的逻辑
但这个方案有点缺憾。对于大部分App来说文件全部备份和恢复就行了,不需要搞特别的定制。但实际情况是需要支持备份恢复的话,就得各自实现一套ContentProvider去做文件的收集和覆盖。
而Google采取的方案是这样的。默认认为每个App都支持Backup功能,然后给App提供同样的BackupAgent去执行自动备份和恢复的处理。当App存在特别定制的需求的时候可以指定扩展的BackupAgent逻辑,更加灵活高效。
内部的实现原理简述如下。
- 系统服务BMS(
BackupManagerService
)收到BackupManager
API发起的备份/恢复请求后,该服务将通过IBackupTransport
和Cloud端建立连接 - BMS通过持有的
BackupHandler
依据操作参数启动相应的Backup或Restore线程 - 任务线程通过
AppBackupUtils
检查该App是否支持Backup/Restore - 之后依据备份模式创建对应的Engine并通过通过
IBackupAgent
向App发起实际的操作请求 BackupAgent
将按照对应模式去读取或写入文件
3. 发起、调试和解密Backup
3.1 Backup/Restore的发起
3.1.1 代码方式
选取了键值对备份模式的话,需要在数据发生变动的时候手动发起备份。SDK提供了API:BackupManager的dataChanged()
。调用之后BackupManager将调度备份请求在适当的时机发起备份处理。
事实上BackupManager还提供了requestRestore()
供我们手动发起恢复,API的返回值将告诉我们是否将要执行恢复操作。这个API发起的恢复结束之后不会KILL进程,存在造成数据错乱的隐患,最好依赖于系统自行的恢复操作。 ※自Android 9.0开始这个API废弃了,调用了也没有反应。
3.1.2 命令方式
ⅰ. adb命令
adb的backup和restore命令可以帮助我们手动发起较为简单的请求,但只支持自动备份模式。
- Backup
// 比如备份某个App的数据并以指定的名称保存备份文件
adb backup -f .ab -apk
接下来系统会提示我们输入备份密码。
输完密码之后点击开始备份,系统将弹出备份开始或结束的Toast。当然不输入密码直接备份也是可以的,但备份的数据容易被破解。
- Restore
// 发起恢复请求的命令很简单
adb restore .ab
接下来输入密码开始恢复,同样的会有Toast提示恢复的进度。
ⅱ.bmgr工具
adb backup命令提供的功能不够强大,官方推荐bmgr
工具。它将备份和恢复的步骤分得更细,便于我们理清各个环节,更好的协助我们测试备份和恢复的逻辑。
bmgr工具没有UI,完全通过命令在后台默默运行。
首先需要启用它。 注意:要确保设置里的Backup功能没有被关闭,Settings > Backup & Restore。
adb shell bmgr enabled
Backup Manager currently enabled
接着,查看ROM里支持的文件传输服务,*号表示当前选择的服务。
adb shell bmgr list transports
com.android.localtransport/.LocalTransport
com.google.android.gms/.backup.migrate.service.D2dTransport
- com.google.android.gms/.backup.BackupTransportService
GMS的传输服务要求设备联网和科学上网,为方面测试我们切换服务为本地传输。
adb shell bmgr transport com.android.localtransport/.LocalTransport
Selected transport com.android.localtransport/.LocalTransport (formerly com.google.android.gms/.backup.BackupTransportService)
查看传输服务的更改是否生效。
adb shell bmgr list transports
- com.android.localtransport/.LocalTransport
com.google.android.gms/.backup.migrate.service.D2dTransport
com.google.android.gms/.backup.BackupTransportService
针对某个App发起备份。
adb shell bmgr backupnow
在另一个终端捕捉备份的执行日志,有可能会提示没有设置锁屏密码。
Backup : [CryptoEnableCheck] Should not encrypt backups: device has no lock screen.
设置密码后再次发起备份,可以看到成功备份了。
adb shell bmgr backupnow
Package xxx with result: Success
Backup finished with result: Success
日志终端也显示回调了App指定的BackupAgent
。
AndroidRuntime: Calling main entry com.android.commands.bmgr.Bmgr
PFTBT : backupmanager pftbt token=4081832e
BackupManagerService: awaiting agent for ApplicationInfo{30f779b xxx}
BackupRestoreAgent: MyBackupAgent()
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=xxx agent=android.os.BinderProxy@5f88b66
BackupManagerService: got agent android.app.IBackupAgent
S
t
u
b
Stub
StubProxy@c309ea7
BackupRestoreAgent: onBackup()
BackupRestoreAgent: onDestroy()
bmgr工具在手动恢复的时候需要Token信息,通过dumpsys backup
获取对应的Token
。Token来自于Ancestral
和Current
两个标签的组合,比如本次的Token为01。
adb shell dumpsys backup
Backup Manager is enabled / setup complete / not pending init
Auto-restore is enabled
No backups running
Last backup pass started: 1619317275335 (now = 1619319671619)
next scheduled: 1619332172012
…
Ancestral: 0 ★
Current: 1 ★
…
清空App数据。
adb shell pm clear
手动恢复数据,从命令和日志两个终端都能看到数据被正确恢复了。
adb shell bmgr restore 01
Scheduling restore: Local disk image
restoreStarting: 1 packages
onUpdate: 0 = com.example.alldemo
restoreFinished: 0
done
BackupRestoreAgent: MyBackupAgent()
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=com.example.alldemo agent=android.os.BinderProxy@a480a0c
BackupManagerService: got agent android.app.IBackupAgent
S
t
u
b
Stub
StubProxy@5041f55
BackupManagerService: initiateOneRestore packageName=xxx
BackupRestoreAgent: onRestore()
BackupManagerService: restoreFinished packageName=xxx
BackupRestoreAgent: onRestoreFinished()
BackupManagerService: Restore complete, killing host process of xxx ★
BackupRestoreAgent: onDestroy()
BackupManagerService: No more packages; finishing restore
BackupManagerService: Restore complete.
当然将App卸载后通过市场或手动安装可以自动地恢复数据,这个动作由系统在Apk安装的时候自动完成。
Transport服务的选择要小心。如果选了GMS Transport的话,要注意GMS场景的网络问题,不然备份会失败。
更加详细的bmgr使用方法可参考如下文档。
developer.android.google.cn/studio/comm…
3.1.3 Google发起
Google将会按照每日一次的频次对支持自动备份模式的App发起备份操作。
恢复的话则是在设备第一次开机登录Google账号后。Google会将数据从服务器下载通过BackupManager
向各个备份过的App发起恢复操作。尚未安装的App则在后期Apk安装完成之后由Google自行发起恢复。
3.2 Backup/Restore的调试
logcat指定BackupManagerService
的Tag,可以监听到Backup和Restore的日志,辅助我们把握操作的进度和报错的原因。
adb logcat -s BackupManagerService
比如针对Google Photos App进行adb备份和恢复操作的时候,将会输出如下日志。
- Backup
adb logcat -s BackupManagerService
BackupManagerService: Requesting backup: apks=true obb=false shared=false all=false system=true includekeyvalue=false pkgs=[Ljava.lang.String;@190020e
BackupManagerService: Beginning adb backup…
BackupManagerService: Starting backup confirmation UI, token=1441721864
BackupManagerService: Waiting for backup completion…
BackupManagerService: acknowledgeAdbBackupOrRestore : token=1441721864 allow=true
BackupManagerService: — Performing adb backup —
BackupManagerService: Package com.google.android.apps.photos is key-value.
BackupManagerService: Adb backup processing complete.
BackupManagerService: Full backup pass complete.
- Restore
adb logcat -s BackupManagerService
BackupManagerService: Beginning restore…
BackupManagerService: Starting restore confirmation UI, token=1694423050
BackupManagerService: Waiting for restore completion…
BackupManagerService: acknowledgeAdbBackupOrRestore : token=1694423050 allow=true
BackupManagerService: — Performing full-dataset restore —
BackupManagerService: adb restore processing complete.
BackupManagerService: Full restore pass complete.
一般来说BackupManagerService
提供的日志情报足够了,但在调试Transport,使用bmgr工具等场景的时候,还可以使用这些Tag获得更详细的日志:Backup,BackupManager,PFTBT,GmsBackupTransport,PerformBackupTask和RestoreSession等。
adb logcat -s AndroidRuntime -s Backup -s BackupManager -s BackupManagerService -s PFTBT -s GmsBackupTransport -s -s PerformBackupTask -s RestoreSession
3.3 Backup文件的解密
Backup文件的后缀名为.ab,估计是android backup的缩写。我们用Text打开上面备份的Google Photos文件,可以看到如下信息。
ANDROID BACKUP
5
1
AES-256
C356E772D89C31C0FCAE6BF16BEC2FF90F0503BCD12111B380FF6054B823D80963EEDC661D92DB908788B48499A80B62731C1A9822C8BF5CD8D67AE85FF45CD9
…
整个文件内容包含头和内容,其中头的信息非常重要,关乎到备份的策略和解密的方式。
- Backup功能的版本号,比如上面的5,定义在源码的
UserBackupManagerService
文件中 - Backup备份文件是否压缩,比如上面的1意味着经过了压缩
- Backup加密方式,比如上面采用了
AES-256
加密算法,如果未输入密码备份的话,此处会显示none
未输入密码的ab文件。
ANDROID BACKUP
5
1
none
xレb
…
我们可以使用abe.jar
来解密备份的文件,如果使用了加密算法的话,还需要Java Cryptography Extension
jar包的帮助。
这里简单演示下没有加密的备份文件的破解过程。
// 输入如下命令
java -jar abe.jar unpack backupFileName-nopwd.ab backupFileName-nopwd.tar
未输出任何Exception则表示解密成功,并会生成指定的tar包。解压出来之后是包括DB、SP在内的原始数据。
abe.jar全名为android-backup-extractor
,是采用Java语言编写的转为解密Android备份文件的工具,非常好用。
abe.jar下载地址
除了这个工具,貌似DD
命令也可以破解,笔者没有试过,感兴趣的可以参考如下文章进行更深入的尝试。
浅谈安卓系统备份文件ab格式解析
4. 实战
铺垫了关于Backup功能的大量知识,就是想让完整地认识和理解这个功能。接下来进入最实用的实战环节。
4.1 准备工作
4.1.1 思考Backup的需求
在定制所需的Backup功能前,先了解清楚自己的Backup需求,比如尝试问自己如下几个问题。
- 备份的数据Size会很大吗?超过5M甚至25M吗?
- 应用的数据全部都需要备份吗?
- 如果数据很大,需要对应用的部分数据做出取舍,哪些数据可以舍弃?
- 如果恢复的数据的版本不同,能直接恢复吗?该怎么定制?
- 定制后的数据能保证继续读写吗?
4.1.2 准备测试Demo
我们先做个涉及到Data
、File
、DB
以及SP
这四种类型数据的App,后面针对这个Demo进行各种Backup功能的定制演示。
Demo通过Jetpack Hilt
完成依赖注入,写入数据的逻辑简述如下:
- 首次打开的时候尚未产生数据,点击Init Button后会将预设的电影海报保存到Data目录,电影Bean实例序列化到File目录,同时通过
Jetpack Room
将该实例保存到DB。如果三个操作成功执行将初始化成功的Flag标记到SP文件 - 再次打开的时候依据SP的Flag将会直接读取这四种类型的数据反映到UI上
Demo地址:github.com/ellisonchan…
4.2 选择备份模式
如果Backup需求不复杂,那优先选择自动备份
模式。因为这个模式提供的空间更大、定制也更灵活。是Google首推的Backup模式。
如果应用数据Size很小而且愿意手动实现DB文件的备份恢复逻辑的话,可以采用键值对备份
模式。
4.3 自动备份
鉴于键值对备份的诸多不足,Google在6.0推出的自动备份
模式带来了很多改善。
- 自动执行无需手动发起
- 更大的备份空间(由原来的5M变成了25M)
- 更多类型文件的支持(在File和SP文件以外还支持了Data和DB文件)
- 更简单的备份规则(通过XML即可快速指定备份对象)
- 更安全的备份条件(在规则中指定flag可限定备份执行的条件)
ⅰ. 基本定制
想要支持自动备份模式的话,什么代码也不用写,因为6.0开始自动备份模式默认打开。但我还是推荐开发者明确地打开allowBackup
属性,这表示你确实意识到Backup功能并决定支持它。
<manifest … >
<application android:allowBackup=“true” … />
开启之后同样使用adb命令模拟备份恢复的过程,通过截图可以看到所有数据都被完整恢复了
// Backup
adb backup -f auto-backup.ab -apk com.ellison.backupdemo
// Clear data
adb shell pm clear com.ellison.backupdemo
// Restore
adb restore auto-backup.ab
ⅱ. 简单的备份规则
通过fullBackupContent
属性可以指向包含备份规则的 XML 文件。我们可以在规则里决定了备份哪些文件,无视哪些文件。
比如只需要备份放在Data的海报图片和SP,不需要File和DB文件。
<manifest … >
<application android:allowBackup=“true”
android:fullBackupContent="@xml/my_backup_rules" … />
运行下备份和恢复的命令可以看到如下File和DB确实没有备份成功。
ⅲ.补充规则所需的条件
当某些隐私程度极高的数据,不放心被备份在网络里,但如果数据被加密的话可以考虑。面对这种有条件的备份,Google提供了requireFlags
属性来解决。
通过在XML规则里给属性指定如下value可以补充备份操作的额外条件。
clientSideEncryption
:只在手机设置了密码等密钥的情况下执行备份deviceToDeviceTransfer
:只在D2D的设备间备份的情况下执行备份
在上述规则上增加一个条件:只在设备设置密码的情况下备份海报图片。
...如果设备未设置密码,运行下备份和恢复的命令可以看到图片确实也被没有备份。 可是设置了密码,而且打开了Backup功能,无论使用backup命令还是bmgr工具都没能将图片备份。clientSideEncryption的真正条件看来没能被满足,后期继续研究。
如果您已将开发设备升级到 Android 9,则需要在升级后停用数据备份功能,然后再重新启用。这是因为只有当在“设置”或“设置向导”中通知用户后,Android 才会使用客户端密钥加密备份。
ⅳ.定制备份的流程
如果XML定制备份规则的方案还不能满足需求的话,可以像键值对备份模式一样指定BackupAgent,来更灵活地控制备份流程。
可是指定了BackupAgent的话默认会变成键值对备份模式。我们如果仍想要更优的自动备份模式怎么办?Google考虑到了这点,只需再打开fullBackupOnly
这个属性。(像极了我们改Bug时候不断引入新Flag的操作。。。)
<manifest … >
…
<application android:allowBackup=“true”
android:backupAgent=".MyBackupAgent"
android:fullBackupOnly=“true” … />
class MyBackupAgent: BackupAgentHelper() {
override fun onCreate() {
Log.d(Constants.TAG_BACKUP, “onCreate()”)
super.onCreate()
}
override fun onDestroy() {
Log.d(Constants.TAG_BACKUP, “onDestroy()”)
super.onDestroy()
}
override fun onFullBackup(data: FullBackupDataOutput?) {
Log.d(Constants.TAG_BACKUP, “onFullBackup()”)
super.onFullBackup(data)
}
override fun onRestoreFile(…
) {
Log.d(Constants.TAG_BACKUP, “onRestoreFile() destination:
d
e
s
t
i
n
a
t
i
o
n
t
y
p
e
:
destination type:
destinationtype:type mode:
m
o
d
e
m
t
i
m
e
:
mode mtime:
modemtime:mtime”)
super.onRestoreFile(data, size, destination, type, mode, mtime)
}
// Callback when restore finished.
override fun onRestoreFinished() {
Log.d(Constants.TAG_BACKUP, “onRestoreFinished()”)
super.onRestoreFinished()
}
}
这样子便可以在定制Backup流程的依然采用自动备份模式,两全其美。
adb backup -f auto-backup.ab -apk com.ellison.backupdemo
adb logcat -s BackupManagerService -s BackupRestoreAgent
BackupRestoreAgent: MyBackupAgent()
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@3c0bc60
BackupManagerService: got agent android.app.IBackupAgent S t u b Stub StubProxy@4b5a519
BackupManagerService: Calling doFullBackup() on com.ellison.backupdemo
BackupRestoreAgent: onFullBackup() ★
BackupManagerSe
rvice: Adb backup processing complete.
BackupRestoreAgent: onDestroy()
AndroidRuntime: Shutting down VM
BackupManagerService: Full backup pass complete. ★
注意: 6.0之前的系统尚未支持自动备份模式,allowBackup
打开也只支持键值对模式。而fullBackupOnly
属性的补充设置也会被系统无视。
ⅴ.进阶定制之限制备份来源
与中国市场上大都售卖无锁版设备不同,海外售卖的不少设备是绑定运营商的。而不同运营商上即便同一个应用,它们预设的数据可能都不同。这时候我们可能需要对备份数据的来源做出限制。
简言之A设备上面备份数据限制恢复到B设备。
如何实现
因为自动备份模式下不会将数据的appVersionCode
传回来,所以判断应用版本的办法行不通。而且有的时候应用版本是一致的,只是运营商不一致。
所以需要我们自己实现,大家可以自行思考。先说我之前想到的几种方案。
- 备份的时候将设备的名称埋入SP文件,恢复的时候检查SP文件里的值
- 备份的时候将设备的名称埋入新的File文件,恢复的时候检查File文件的值
这俩方案的缺陷: 方案1的缺点在于备份的逻辑会在原有的文件里增加值,会影响现有的逻辑。
方案2增加了新文件,避免对现有的逻辑造成影响,对方案1有所改善。但它和方案1都存在一个潜在的问题。
问题在于无法保证这个新文件首先被恢复到,也就无保证在恢复执行的一开始就知道本次恢复是否需要。
假使恢复进行到了一半,轮到标记新文件的时候才发现本次恢复需要丢弃,那么将会导致数据错乱。因为系统没有提供Roll back已恢复数据的API,如果我们自己也没做好保存和回退旧的文件处理的话,最后必然发生部分文件已恢复部分没恢复的不一致问题。
要理解这个问题就要搞清楚恢复操作针对文件的执行顺序。
自动备份模式在恢复的时候会逐个调用onRestoreFile()
,将各个目录下备份的文件回调过来。目录之间的顺序和备份时候的顺序一致,如下备份的代码可以看出来:从根目录的Data开始,接着File目录开始,然后DB和SP文件。
public abstract class BackupAgent extends ContextWrapper {
…
public void onFullBackup(FullBackupDataOutput data) throws IOException {
…
// Root dir first.
applyXmlFiltersAndDoFullBackupForDomain(
packageName, FullBackup.ROOT_TREE_TOKEN, manifestIncludeMap,
manifestExcludeSet, traversalExcludeSet, data);
// Data dir next.
traversalExcludeSet.remove(filesDir);
// Database directory.
traversalExcludeSet.remove(databaseDir);
// SharedPrefs.
traversalExcludeSet.remove(sharedPrefsDir);
}
}
文件内的顺序则通过File#list()
获取,而这个API是无法保证得到的文件列表都按照abcd的字母排序。所以在File目录下放标记文件不能保证它首先被恢复到。即便放一个a开头的标记文件也不能完全保证。
★推荐方案★
一般的App鲜少在根目录存放数据,而根目录最先被恢复到。所以我推荐的方案是这样的。
备份的时候将设备的名称埋入根目录的特定文件,恢复的时候检查该File文件,在恢复的初期就决定本次恢复是否需要。为了不影响恢复之后的正常使用,最后还要删除这个标记文件。
废话不多说,看下代码。
- Backup里放入标记文件。
class MyBackupAgent : BackupAgentHelper() {
…
override fun onFullBackup(data: FullBackupDataOutput?) {
// ★ 在备份执行前先将标记文件写入Data目录
// Make backup source file before full backup invoke.
writeBackupSourceToFile()
super.onFullBackup(data)
}
private fun writeBackupSourceToFile() {
val sourceFile = File(dataDir.absolutePath + File.separator
- Constants.BACKUP_SOURCE_FILE_PREFIX + Build.MODEL)
if (!sourceFile.exists()) {
sourceFile.createNewFile()
}
}
…
}
- Restore检查标记文件。
class MyBackupAgent : BackupAgentHelper() {
private var needSkipRestore = false
…
override fun onRestoreFile(
data: ParcelFileDescriptor?,
size: Long,
destination: File?,
type: Int,
mode: Long,
mtime: Long
) {
if (!needSkipRestore) {
val sourceDevice = readBackupSourceFromFile(destination)
// ★ 备份源设备名和当前名不一致的时候标记需要跳过
// Mark need skip restore if source got and not match current device.
if (!TextUtils.isEmpty(sourceDevice) && !sourceDevice.equals(Build.MODEL)) {
needSkipRestore = true
}
}
if (!needSkipRestore) {
// Invoke restore if skip flag set.
super.onRestoreFile(data, size, destination, type, mode, mtime)
} else {
// ★ 跳过备份但一定要消费stream防止恢复的进程阻塞
// Consume data to keep restore stream go.
consumeData(data!!, size, type, mode, mtime, null)
}
}
…
private fun readBackupSourceFromFile(file: File?): String {
if (file == null) return “”
var decodeDeviceSource = “”
// Got data file with backup source mark.
if (file.name.startsWith(Constants.BACKUP_SOURCE_FILE_PREFIX)) {
decodeDeviceSource = file.name.replace(Constants.BACKUP_SOURCE_FILE_PREFIX, “”)
}
return decodeDeviceSource
}
@Throws(IOException::class)
fun consumeData(data: ParcelFileDescriptor,
size: Long, type: Int, mode: Long, mtime: Long, outFile: File?) {
…
}
}
- 无论是Backup还是Restore都要将标记文件移除。
class MyBackupAgent : BackupAgentHelper() {
…
override fun onDestroy() {
super.onDestroy()
// 移除标记文件
// Ensure temp source file is removed after backup or restore finished.
ensureBackupSourceFileRemoved()
}
private fun ensureBackupSourceFileRemoved() {
Source = “”
// Got data file with backup source mark.
if (file.name.startsWith(Constants.BACKUP_SOURCE_FILE_PREFIX)) {
decodeDeviceSource = file.name.replace(Constants.BACKUP_SOURCE_FILE_PREFIX, “”)
}
return decodeDeviceSource
}
@Throws(IOException::class)
fun consumeData(data: ParcelFileDescriptor,
size: Long, type: Int, mode: Long, mtime: Long, outFile: File?) {
…
}
}
- 无论是Backup还是Restore都要将标记文件移除。
class MyBackupAgent : BackupAgentHelper() {
…
override fun onDestroy() {
super.onDestroy()
// 移除标记文件
// Ensure temp source file is removed after backup or restore finished.
ensureBackupSourceFileRemoved()
}
private fun ensureBackupSourceFileRemoved() {
最后
以上就是深情毛巾为你收集整理的全面复盘Android开发者容易忽视的Backup功能 _ 创作者训练营第二期的全部内容,希望文章能够帮你解决全面复盘Android开发者容易忽视的Backup功能 _ 创作者训练营第二期所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复