写点什么

全面复盘 Android 开发者容易忽视的 Backup 功能 _ 创作者训练营第二期

用户头像
Android架构
关注
发布于: 12 小时前
  • 恢复则是调度 App 将文件解密之后通过 ContentProvider 再回传给各 App,各 App 自行执行恢复的逻辑


但这个方案有点缺憾。对于大部分 App 来说文件全部备份和恢复就行了,不需要搞特别的定制。但实际情况是需要支持备份恢复的话,就得各自实现一套 ContentProvider 去做文件的收集和覆盖。


而 Google 采取的方案是这样的。默认认为每个 App 都支持 Backup 功能,然后给 App 提供同样的 BackupAgent 去执行自动备份和恢复的处理。当 App 存在特别定制的需求的时候可以指定扩展的 BackupAgent 逻辑,更加灵活高效。


内部的实现原理简述如下。


  1. 系统服务 BMS(BackupManagerService)收到BackupManager API 发起的备份/恢复请求后,该服务将通过IBackupTransport和 Cloud 端建立连接

  2. BMS 通过持有的BackupHandler依据操作参数启动相应的 Backup 或 Restore 线程

  3. 任务线程通过AppBackupUtils检查该 App 是否支持 Backup/Restore

  4. 之后依据备份模式创建对应的 Engine 并通过通过IBackupAgent向 App 发起实际的操作请求

  5. 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


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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 来自于AncestralCurrent两个标签的组合,比如本次的 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

我们先做个涉及到DataFileDB以及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传回来,所以判断应用版本的办法行不通。而且有的时候应用版本是一致的,只是运营商不一致。


所以需要我们自己实现,大家可以自行思考。先说我之前想到的几种方案。


  1. 备份的时候将设备的名称埋入 SP 文件,恢复的时候检查 SP 文件里的值

  2. 备份的时候将设备的名称埋入新的 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 这种大空间文件的备份处理,并不推荐使用。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
全面复盘Android开发者容易忽视的Backup功能 _ 创作者训练营第二期