全面复盘 Android 开发者容易忽视的 Backup 功能 _ 创作者训练营第二期
恢复则是调度 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 <fileName>.ab -apk <packageName>
接下来系统会提示我们输入备份密码。
输完密码之后点击开始备份,系统将弹出备份开始或结束的 Toast。当然不输入密码直接备份也是可以的,但备份的数据容易被破解。
Restore
// 发起恢复请求的命令很简单 adb restore <fileName>.ab
接下来输入密码开始恢复,同样的会有 Toast 提示恢复的进度。
ⅱ.bmgr 工具
adb backup 命令提供的功能不够强大,官方推荐bmgr
工具。它将备份和恢复的步骤分得更细,便于我们理清各个环节,更好的协助我们测试备份和恢复的逻辑。
bmgr 工具没有 UI,完全通过命令在后台默默运行。
首先需要启用它。 注意:要确保设置里的 Backup 功能没有被关闭,Settings > Backup & Restore。
adb shell bmgr enabledBackup Manager currently enabled
接着,查看 ROM 里支持的文件传输服务,*号表示当前选择的服务。
adb shell bmgr list transportscom.android.localtransport/.LocalTransportcom.google.android.gms/.backup.migrate.service.D2dTransport
com.google.android.gms/.backup.BackupTransportService
GMS 的传输服务要求设备联网和科学上网,为方面测试我们切换服务为本地传输。
adb shell bmgr transport com.android.localtransport/.LocalTransportSelected transport com.android.localtransport/.LocalTransport (formerly com.google.android.gms/.backup.BackupTransportService)
查看传输服务的更改是否生效。
adb shell bmgr list transports
com.android.localtransport/.LocalTransportcom.google.android.gms/.backup.migrate.service.D2dTransportcom.google.android.gms/.backup.BackupTransportService
针对某个 App 发起备份。
adb shell bmgr backupnow <package>
在另一个终端捕捉备份的执行日志,有可能会提示没有设置锁屏密码。
Backup : [CryptoEnableCheck] Should not encrypt backups: device has no lock screen.
设置密码后再次发起备份,可以看到成功备份了。
adb shell bmgr backupnow <package>Package xxx with result: SuccessBackup finished with result: Success
日志终端也显示回调了 App 指定的BackupAgent
。
AndroidRuntime: Calling main entry com.android.commands.bmgr.BmgrPFTBT : backupmanager pftbt token=4081832eBackupManagerService: awaiting agent for ApplicationInfo{30f779b xxx}Back
upRestoreAgent: MyBackupAgent()BackupRestoreAgent: onCreate()BackupManagerService: agentConnected pkg=xxx agent=android.os.BinderProxy@5f88b66BackupManagerService: got agent android.app.IBackupAgentProxy@c309ea7BackupRestoreAgent: onBackup()BackupRestoreAgent: onDestroy()
bmgr 工具在手动恢复的时候需要 Token 信息,通过dumpsys backup
获取对应的Token
。Token 来自于Ancestral
和Current
两个标签的组合,比如本次的 Token 为 01。
adb shell dumpsys backupBackup Manager is enabled / setup complete / not pending initAuto-restore is enabledNo backups runningLast backup pass started: 1619317275335 (now = 1619319671619)next scheduled: 1619332172012...Ancestral: 0 ★Current: 1 ★...
清空 App 数据。
adb shell pm clear <package>
手动恢复数据,从命令和日志两个终端都能看到数据被正确恢复了。
adb shell bmgr restore 01 <package>Scheduling restore: Local disk imagerestoreStarting: 1 packagesonUpdate: 0 = com.example.alldemorestoreFinished: 0done
BackupRestoreAgent: MyBackupAgent()BackupRestoreAgent: onCreate()BackupManagerService: agentConnected pkg=com.example.alldemo agent=android.os.BinderProxy@a480a0cBackupManagerService: got agent android.app.IBackupAgentProxy@5041f55BackupManagerService: initiateOneRestore packageName=xxxBackupRestoreAgent: onRestore()BackupManagerService: restoreFinished packageName=xxxBackupRestoreAgent: onRestoreFinished()BackupManagerService: Restore complete, killing host process of xxx ★BackupRestoreAgent: onDestroy()BackupManagerService: No more packages; finishing restoreBackupManagerService: 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 BackupManagerServiceBackupManagerService: Requesting backup: apks=true obb=false shared=false all=false system=true includekeyvalue=false pkgs=[Ljava.lang.String;@190020eBackupManagerService: Beginning adb backup...BackupManagerService: Starting backup confirmation UI, token=1441721864BackupManagerService: Waiting for backup completion...BackupManagerService: acknowledgeAdbBackupOrRestore : token=1441721864 allow=trueBackupManagerService: --- 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 BackupManagerServiceBackupManagerService: Beginning restore...BackupManagerService: Starting restore confirmation UI, token=1694423050BackupManagerService: Waiting for restore completion...BackupManagerService: acknowledgeAdbBackupOrRestore : token=1694423050 allow=trueBackupManagerService: --- 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 BACKUP51AES-256C356E772D89C31C0FCAE6BF16BEC2FF90F0503BCD12111B380FF6054B823D80963EEDC661D92DB908788B48499A80B62731C1A9822C8BF5CD8D67AE85FF45CD9...
整个文件内容包含头和内容,其中头的信息非常重要,关乎到备份的策略和解密的方式。
Backup 功能的版本号,比如上面的 5,定义在源码的
UserBackupManagerService
文件中Backup 备份文件是否压缩,比如上面的 1 意味着经过了压缩
Backup 加密方式,比如上面采用了
AES-256
加密算法,如果未输入密码备份的话,此处会显示none
未输入密码的 ab 文件。
ANDROID BACKUP51nonex?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" ... /></manifest>
开启之后同样使用 adb 命令模拟备份恢复的过程,通过截图可以看到所有数据都被完整恢复了
// Backup
adb backup -f auto-backup.ab -apk com.ellison.backupdemo// Clear dataadb shell pm clear com.ellison.backupdemo// Restoreadb restore auto-backup.ab
ⅱ. 简单的备份规则
通过fullBackupContent
属性可以指向包含备份规则的 XML 文件。我们可以在规则里决定了备份哪些文件,无视哪些文件。
比如只需要备份放在 Data 的海报图片和 SP,不需要 File 和 DB 文件。
<manifest ... ><application android:allowBackup="true"android:fullBackupContent="@xml/my_backup_rules" ... /></manifest>
运行下备份和恢复的命令可以看到如下 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" ... /></manifest>
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:type mode: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 BackupRestoreAgentBackupRestoreAgent: MyBackupAgent() BackupRestoreAgent: onCreate()BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@3c0bc60BackupManagerService: got agent android.app.IBackupAgentProxy@4b5a519BackupManagerService: Calling doFullBackup() on com.ellison.backupdemoBackupRestoreAgent: onFullBackup() ★BackupManagerService: Adb backup processing complete.BackupRestoreAgent: onDestroy()AndroidRuntime: Shutting down VMBackupManagerService: 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() {val sourceFile = File(dataDir.absolutePath + File.separator
Constants.BACKUP_SOURCE_FILE_PREFIX + Build.MODEL)if (sourceFile.exists()) {val result = sourceFile.delete()}}}
接下里验证代码能否拦截不同设备的备份文件。先在小米手机里备份文件,然后到 Pixel 模拟器里恢复这个数据。
小米里备份
adb -s c7a1a50c7d27 backup -f auto-backup-cus-xiaomi.ab -apk com.ellison.backupdemo
adb -s c7a1a50c7d27 logcat -s BackupManagerService -s BackupRestoreAgentBackupManagerService: --- Performing full backup for package com.ellison.backupdemo ---BackupRestoreAgent: onCreate()BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@5e68506BackupManagerService: got agent android.app.IBackupAgentProxy@852a7c7BackupManagerService: Calling doFullBackup() on com.ellison.backupdemoBackupRestoreAgent: onFullBackup()// ★标记文件里写入了小米的设备名称并备份了 BackupRestoreAgent: writeBackupSourceToFile() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A create:true ★BackupRestoreAgent: onDestroy()BackupManagerService: Adb backup processing complete.BackupRestoreAgent: ensureBackupSourceFileRemoved() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A delete:true ★BackupManagerService: Full backup pass complete.
Pixel 里恢复,可以看到 Pixel 的日志里显示跳过了恢复
adb -s emulator-5554 restore auto-backup-cus-xiaomi.ab
adb -s emulator-5554 logcat -s BackupManagerService -s BackupRestoreAgentBackupManagerService: --- Performing full-dataset restore ---...BackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/backup-source-Redmi 6A type:1 mode:384 mtime:1619355877 currentDevice:sdk_gphone_x86_arm needSkipRestore:falseBackupRestoreAgent: readBackupSourceFromFile() file:/data/data/com.ellison.backupdemo/backup-source-Redmi 6ABackupRestoreAgent: readBackupSourceFromFile() source:Redmi 6ABackupRestoreAgent: onRestoreFile() sourceDevice:Redmi 6A// ★从备份数据里读取到了小米的设备名,不同于 Pixel 模拟器的名称,设定了跳过恢复的 flagBackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/Post.jpg type:1 mode:384 mtime:1619355781 currentDevice:sdk_gphone_x86_arm needSkipRestore:trueBackupRestoreAgent: onRestoreFile() skip restore and consume ★...BackupRestoreAgent: onRestoreFinished()BackupManagerService: [UserID:0] adb restore processing complete.BackupRestoreAgent: onDestroy()BackupManagerService: Full restore pass complete.
Pixel 模拟器上重新打开 App 之后确实没有任何数据。
当然如果 App 确实有在根目录下存放数据,那么建议你仍采用这个方案。
只不过需要给这个特定文件加一个 a 的前缀,以保证它大多数情况下会被先恢复到。当然为了防止极低的概率下它没有首先被恢复,开发者还需自行加上一个 Data 目录下文件的暂存和回退处理,以防万一。
更高的定制需求
如果发现备份的设备名称不一致的时候,客户的需求并不是丢弃恢复,而是让我们将运营商之间的 diff merge 进来呢?
这里提供一个思路。在上述方案的基础之上改下就行了。
比如恢复的一开始通过标记的文件发现备份的不一致,丢弃恢复的同时将待恢复的文件都改个别名暂存到本地。应用再次打开的时候读取暂存的数据和当前数据做对比,然后将 diff merge 进来。
ⅵ.BackupAgent 和配置规则的混用
BackupAgent 和 XML 配置并不冲突,在 backup 逻辑里还可以获取配置的设备条件。比如在 onFullBackup()里可以利用 FullBackupDataOutput 的 getTransportFlags()来取得相应的 Flag 来执行相应的逻辑。
FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED 对应着设备加密条件
FLAG_DEVICE_TO_DEVICE_TRANSFER 对应 D2D 备份场景条件
class MyBackupAgent: BackupAgentHelper() {...override fun onFullBackup(data: FullBackupDataOutput?) {Log.d(Constants.TAG_BACKUP, "onFullBackup()")super.onFullBackup(data)
if (data != null) {if ((data.transportFlags and FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED) != 0) {Log.d(Constants.TAG_BACKUP, "onFullBackup() CLIENT ENCRYPTION NEED")}}}}
4.4 键值对备份
键值对备份
支持的空间小,而且针对 File 类型的 Backup 实现非线程安全
,同时需要自行考虑 DB 这种大空间文件的备份处理,并不推荐使用。
评论