全面复盘Android开发者容易忽视的Backup功能(2)

本文涉及的产品
阿里云百炼推荐规格 ADB PostgreSQL,4核16GB 100GB 1个月
日志服务 SLS,月写入数据量 50GB 1个月
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 全面复盘Android开发者容易忽视的Backup功能(2)

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地址:https://github.com/ellisonchan/BackupRestoreApp

20200308234636925.jpg

20200308234636925.jpg

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 data
>adb shell pm clear com.ellison.backupdemo
// Restore
>adb restore auto-backup.ab

20200308234636925.jpg

ⅱ. 简单的备份规则

通过fullBackupContent属性可以指向包含备份规则的 XML 文件。我们可以在规则里决定了备份哪些文件,无视哪些文件。

比如只需要备份放在Data的海报图片和SP,不需要File和DB文件。

<manifest ... >
    <application android:allowBackup="true"
        android:fullBackupContent="@xml/my_backup_rules" ... />
</manifest>
<!-- my_backup_rules.xml -->
<full-backup-content>
    <!-- include指定参与备份的文件 -->
    <!-- domain指定root代表这个的规则适用于data目录 -->
    <include domain="root" path="Post.jpg"/>
    <!-- path里指定.代表该目录下所有文件都适用这个规则,免去逐个指定各个文件 -->
    <include domain="sharedpref" path="."/>
    <!-- exclude指定不参与备份的文件 -->
    <exclude domain="file" path="."/>
    <exclude domain="database" path="."/>
</full-backup-content>

运行下备份和恢复的命令可以看到如下File和DB确实没有备份成功。

20200308234636925.jpg

ⅲ.补充规则所需的条件

当某些隐私程度极高的数据,不放心被备份在网络里,但如果数据被加密的话可以考虑。面对这种有条件的备份,Google提供了requireFlags属性来解决。


通过在XML规则里给属性指定如下value可以补充备份操作的额外条件。


clientSideEncryption:只在手机设置了密码等密钥的情况下执行备份

deviceToDeviceTransfer:只在D2D的设备间备份的情况下执行备份

在上述规则上增加一个条件:只在设备设置密码的情况下备份海报图片。

<!-- my_backup_rules.xml -->
<full-backup-content>
    <include domain="root" path="Post.jpg" requireFlags="clientSideEncryption"/>
    ...
</full-backup-content>

如果设备未设置密码,运行下备份和恢复的命令可以看到图片确实也被没有备份。

20200308234636925.jpg

可是设置了密码,而且打开了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:$destination type:$type mode:$mode mtime:$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$Stub$Proxy@4b5a519
BackupManagerService: Calling doFullBackup() on com.ellison.backupdemo
BackupRestoreAgent: onFullBackup() ★
BackupManagerService: Adb backup processing complete.
BackupRestoreAgent: onDestroy()
AndroidRuntime: Shutting down VM
BackupManagerService: Full backup pass complete. ★

注意:

6.0之前的系统尚未支持自动备份模式,allowBackup打开也只支持键值对模式。而fullBackupOnly属性的补充设置也会被系统无视。

ⅴ.进阶定制之限制备份来源

与中国市场上大都售卖无锁版设备不同,海外售卖的不少设备是绑定运营商的。而不同运营商上即便同一个应用,它们预设的数据可能都不同。这时候我们可能需要对备份数据的来源做出限制。

简言之A设备上面备份数据限制恢复到B设备。

20200308234636925.jpg

如何实现

因为自动备份模式下不会将数据的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 BackupRestoreAgent
BackupManagerService: --- Performing full backup for package com.ellison.backupdemo ---
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@5e68506
BackupManagerService: got agent android.app.IBackupAgent$Stub$Proxy@852a7c7
BackupManagerService: Calling doFullBackup() on com.ellison.backupdemo
BackupRestoreAgent: 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 BackupRestoreAgent
BackupManagerService: --- 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:false
BackupRestoreAgent: readBackupSourceFromFile() file:/data/data/com.ellison.backupdemo/backup-source-Redmi 6A
BackupRestoreAgent: readBackupSourceFromFile() source:Redmi 6A
BackupRestoreAgent: onRestoreFile() sourceDevice:Redmi 6A
// ★从备份数据里读取到了小米的设备名,不同于Pixel模拟器的名称,设定了跳过恢复的flag
BackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/Post.jpg type:1  mode:384  mtime:1619355781 currentDevice:sdk_gphone_x86_arm needSkipRestore:true
BackupRestoreAgent: onRestoreFile() skip restore and consume ★
...
BackupRestoreAgent: onRestoreFinished()
BackupManagerService: [UserID:0] adb restore processing complete.
BackupRestoreAgent: onDestroy()
BackupManagerService: Full restore pass complete.

Pixel模拟器上重新打开App之后确实没有任何数据。

20200308234636925.jpg

当然如果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这种大空间文件的备份处理,并不推荐使用。

但本着学习的目的还是要了解一下。

ⅰ. 基本定制

使用这个模式需额外指定BackupAgent并实现其细节。

<manifest ... >
    <application android:allowBackup="true"
                 android:backupAgent=".MyBackupAgent" ... >
        <!-- 为兼容旧版本设备最好加上api_key的meta-data -->
        <meta-data android:name="com.google.android.backup.api_key"
            android:value="unused" />
    </application>
</manifest>

BackupAgent的实现在于告诉BMS每个类型的文件采用什么Key备份和恢复。可以选择高度定制的复杂办法去实现,当然SDK也提供了简单办法。


复杂办法:直接扩展自BackupAgent抽象类,需要自行实现onBackup()和onRestore的细节。包括读取各类型文件并调用对应的Helper实现写入数据到备份文件中以及考虑旧的备份数据的迁移等处理。需要考虑很多细节,代码量很大

简单办法:扩展自系统封装好的BackupAgentHelper类并告知各类型文件对应的KEY和Helper实现即可,高效而简单,但没有提供大容量文件比如DB的备份实现

以扩展BackupAgentHelper的简单办法为例,演示下键值对备份的实现。


SP文件的话SDK提供了特定的SharedPreferencesBackupHelper实现

File文件对应的Helper实现为FileBackupHelper,只限于file目录的数据

其他类型文件比如Data和DB是没有预设Helper实现的,需要自行实现BackupHelper

// MyBackupAgent.kt
class MyBackupAgent: BackupAgentHelper() {
    override fun onCreate() {
        ...
        // Init helper for data, file, db and sp files.
        // Data和DB文件使用FileBackupHelper是无法备份的,此处单纯为了验证下
        FileBackupHelper(this, Constants.DATA_NAME).also { addHelper(Constants.BACKUP_KEY_DATA, it) }
        FileBackupHelper(this, Constants.DB_NAME).also { addHelper(Constants.BACKUP_KEY_DB, it) }
        // File和SP各自使用对应的Helper是可以备份的
        FileBackupHelper(this, Constants.FILE_NAME).also { addHelper(Constants.BACKUP_KEY_FILE, it) }
        SharedPreferencesBackupHelper(this, Constants.SP_NAME).also { addHelper(Constants.BACKUP_KEY_SP, it) }
    }
    ...
}

先用bmgr工具执行Backup,然后清除Demo的数据再执行Restore。从日志可以看出来键值对备份和恢复成功进行了。

// 开启bmgr和设置本地传输服务
>adb shell bmgr enabled
>adb shell bmgr transport com.android.localtransport/.LocalTransport
// Backup
>adb shell bmgr backupnow com.ellison.backupdemo
Running incremental backup for 1 requested packages.
Package @pm@ with result: Success
Package com.ellison.backupdemo with result: Success
Backup finished with result: Success
// 清空数据
>adb shell pm clear com.ellison.backupdemo
// 查看Backup Token
>adb shell dumpsys backup
...
Ancestral: 0
Current:   1
// Restore
>adb shell bmgr restore 01 com.ellison.backupdemo
Scheduling restore: Local disk image
restoreStarting: 1 packages
onUpdate: 0 = com.ellison.backupdemo
restoreFinished: 0
done

Demo的截图显示File和SP备份和恢复成功了。但存放在Data目录的海报和DB目录都失败了。这也验证了上述的结论。

20200308234636925.jpg

因为出于备份文件空间的考虑,官方并不建议针对DB文件等大容量文件做键值对备份。理论上可以扩展FileBackupHelper对Data和DB文件做出支持。但Google将关键的备份实现(FileBackupHelperBase和performBackup_checked())对外隐藏,使得简单扩展变得不可能。


StackOverFlow上针对这个问题有过热烈的讨论,唯一的办法是完全自己实现,但随着自动备份的出现,这个问题似乎已经不再重要。

https://stackoverflow.com/questions/5282936/android-backup-restore-how-to-backup-an-internal-database#

ⅱ.手动发起备份

BackupManager的dataChanged()函数可以告知系统App数据变化了,可以安排备份操作。我们在Demo的Backup Button里添加调用。

class LocalData @Inject constructor(...
                                    val backupManager: BackupManager){
    fun backupData() {
        backupManager.dataChanged()
    }
    ...
}

点击这个Backup Button之后等几秒钟,发现Demo的备份任务被安排进Schedule里,意味着备份操作将被系统发起。

>adb shell dumpsys backup
Pending key/value backup: 3
    BackupRequest{pkg=com.ellison.backupdemo} ★
    ...

我们可以强制这个Schedule的执行,也可以等待系统的调度。

>adb shell bmgr run
BackupManagerService: clearing pending backups
PFTBT   : backupmanager pftbt token=604faa13
...
BackupManagerService: [UserID:0] awaiting agent for ApplicationInfo{7b6a019 com.ellison.backupdemo}
BackupRestoreAgent: onCreate()
BackupManagerService: [UserID:0] agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@be4cabf
BackupManagerService: [UserID:0] got agent android.app.IBackupAgent$Stub$Proxy@4eab58c
BackupRestoreAgent: onBackup() ★
BackupRestoreAgent: onDestroy()
BackupManagerService: [UserID:0] Released wakelock:*backup*-0-1265

20200308234636925.jpg

ⅲ.手动发起恢复

除了bmgr工具提供的restore以外还可以通过代码手动触发恢复。但这并不安全会影响应用的数据一致性,所以恢复的API requestRestore()废弃了。


我们来验证下,在Demo的Restore Button里添加BackupManager#requestRestore()的调用。

class LocalData @Inject constructor(...
                                    val backupManager: BackupManager){
    fun restoreData() {
        backupManager.requestRestore(object: RestoreObserver() {
            ...
        })
    }
    ...
}

但点击Button之后等一段时间,恢复的日志没有出现,反倒是弹出了无效的警告。

BackupRestoreApp: LocalData#restoreData()
BackupManager: requestRestore(): Since Android P app can no longer request restoring of its backup.

ⅳ.备份版本不一致的处理

版本不一致意味着恢复之后的逻辑可能会受到影响,这是我们在定制Backup功能时需要着重考虑的问题。


版本不一致的情况有两种。


现在运行的应用版本比备份时候的版本高,比较常见的场景

现在运行的应用版本比备份时候的版本低,即App降级,不太常见

默认情况下系统会无视App降级的恢复操作,意味着BackupAgent#onRestore()永远不会被回调。


但如果应用对于旧版本数据的兼容处理比较完善,希望支持降级的情况。那么需要在Manifest里打开restoreAnyVersion属性,系统将意识到你的兼容并包并回调你的onRestore处理。


无论哪种情况都可以在BackupAgent#onRestore()回调里拿到备份时的版本。然后读取App当前的VersionCode,执行对应的数据迁移或丢弃处理。

class MyBackupAgent: BackupAgentHelper() {
    ...
    override fun onRestore(
        data: BackupDataInput?,
        appVersionCode: Int,
        newState: ParcelFileDescriptor?
    ) {
        val packageInfo = packageManager.getPackageInfo(packageName, 0)
        if (packageInfo.versionCode != appVersionCode) {
            // Do something.
            // 可以调用BackupDataInput#restoreEntity()
            // 或skipEntityData()决定恢复还是丢弃
        } else {
            super.onRestore(data, appVersionCode, newState)
        }
    }
}

ⅴ.直接扩展BackupAgent

扩展自BackupAgent的需要考虑诸多细节,对这个方案有兴趣的朋友可以参考BackupAgentHelper的源码,也可以查阅官方说明。

https://developer.android.google.cn/guide/topics/data/keyvaluebackup

4.5 系统App的Backup限制

部分系统App的隐私级别较高,即便手动调用了Backup命令,系统仍将无视。并在日志中给出提示。

BackupManagerService: Beginning adb backup...
BackupManagerService: Starting backup confirmation UI, token=1763174695
BackupManagerService: Waiting for backup completion...
BackupManagerService: acknowledgeAdbBackupOrRestore : token=1763174695 allow=true
BackupManagerService: --- Performing adb backup ---
BackupManagerService: Package com.android.phone is not eligible for backup, removing.★提示该App不适合备份操作
BackupManagerService: Adb backup processing complete.
BackupManagerService: Full backup pass complete.

这个限制的源码在AppBackupUtils中,解决办法很简单在Manifest文件里明确指定BackupAgent。


其实Google的意图很清楚,这些系统级别的App数据要是被窃取将十分危险,默认禁止这个操作。但如果你指定了Backup代理那代表开发者考虑到了备份和恢复的场景,对这个操作进行了默许,备份操作才会被放行。

4.6 实战总结

4.6.1 Backup定制的总结

当我们遇到Backup定制任务的时候认真思考下需求再对症下药。为使得这个流程更加直观,做了个流程图分享给大家。

20200308234636925.jpg

4.6.2 Backup相关属性

1672148243643.png

5. Android 12的影响和Backup功能的发展历程

Android 12 Beta版即将公开,其针对Backup功能又做了些改动,先来看看变更的说明。

5.1 D2D 设备到设备备份的规则细分

For apps running on and targeting Android 12 and higher:


Specifying android:allowBackup="false" does disable backups to Google Drive, but doesn’t disable D2D transfers for the app.

Specifying include and exclude rules with the XML configuration mechanism no longer affects D2D transfers, though it still affects Google Drive backups. To specify rules for D2D transfers, you must use the new configuration covered in the next section.

简直之,Android 12开始即便关闭了allowBackup属性,D2D的Backup功能仍将有效,不再受影响。同时原有的通过fullBackupContent指定的配置规则也将失效。


如果你的App目标版本是Android 12的话,需要使用新属性dataExtractionRules来指定语法规则。


语法规则的所变化主要体现在使用新的属性cloud-backup和device-transfer明示地区分云端备份和D2D备份的规则,而不再像之前那样采用full-backup-content指定统一的规则。


另外原有的设备条件flag也发生了变化。


clientSideEncryption:在新规则里变成了disableIfNoEncryptionCapabilities,且只能应用在cloud-backup标签内

deviceToDeviceTransfer:新规则将D2D区分开来了,所以这个flag不需要了

<application
    android:dataExtractionRules="new_config.xml"
    ...>
</application>
<data-extraction-rules>
  <cloud-backup [disableIfNoEncryptionCapabilities="true|false"]>
    <include domain=["file" | "database" | "sharedpref" | "external" |
                        "root"] path="string"/>
  ...
  </cloud-backup>
  <device-transfer>
    <include domain=["file" | "database" | "sharedpref" | "external" |
                        "root"] path="string"/>
    ...
  </device-transfer>
</data-extraction-rules>

原因在于云端备份存在空间的限制,难免需要对备份的文件做出取舍。而D2D的场景文件是存在本地的,没有这种限制了却还对备份文件做出削减显然不太合理。


具体细节可参考官方文档。

https://developer.android.google.cn/about/versions/12/backup-restore

5.2 adb backup命令的限制

To help protect private app data, Android 12 changes the default behavior of the adb backup command. For apps that target Android 12, when a user runs the adb backup command, app data is excluded from any other system data that is exported from the device.

adb backup命令是可以备份整机数据的,从Android 12开始该数据里将不包含App部分的应用数据。除非在Manifest里手动打开debuggable属性。


如果备份单个App也失败的话,那安全性将大大提高。笔者在12 Preview版本上执行该命令仍旧能够正常备份。不知道是不是Target SDK的问题,等正式版出来后再尝试下。


详情可参考官方说明。

https://developer.android.google.cn/about/versions/12/behavior-changes-12

5.3 Backup功能的发展历程

简要回顾下Backup功能的发展历程,供快速查阅。

1672148322963.png

6. 结语

Google针对Backup功能的频繁改动可以看出来其对于这个功能的重视,总结起来就是在功能的易用性,安全性,合理性之间反复优化。


针对这些变化开发者需要不断调整Backup功能的开发策略,我也给出一些实用建议。


思考App是否支持备份,明示地设置allowBackup属性

自动备份模式提供的备份空间更大,定制灵活,更为推荐

隐私级别很高的数据可以添加设备加密的备份条件

复写BackupAgent可以灵活定制备份和恢复的流程,值得好好研究

出于学习和调查的目的可以尝试了解和破解Backup文件

backup命令已不推荐,调试Backup功能尽量尝试功能更为强大的bmgr工具

未决悬念

官方文档说明键值对备份从2.2开始提供支持,可是allowBackup属性自1.6便于导入,那在2.2之前的备份采取哪种模式呢?

想找到一台2.2以前的设备去验证动作不太现实,打算在2.2之前的系统源码里找到答案。

Android 12上目标SDK为12的话如果debuggable未开的话,无论备份整机还是单个app都将失败?

clientSideEncryption表示Backup功能打开且设置了密码均可开始执行备份,但实际测试不是,总是没有执行备份。

DEMO

https://github.com/ellisonchan/BackupRestoreApp

参考资料

备份功能的官方主页

键值对备份模式

自动备份模式

测试备份和恢复

bmgr工具

allowBackup造成的安全问题

Backup文件解密JAR包

Backup文件的解析

键值对备份模式的DB支持

Android 12的行为变更

推荐阅读

Jetpack Hilt有哪些改善又有哪些限制?

Dagger2和它在SystemUI上的应用

除了SQLite一定要试试Room



相关实践学习
阿里云百炼xAnalyticDB PostgreSQL构建AIGC应用
通过该实验体验在阿里云百炼中构建企业专属知识库构建及应用全流程。同时体验使用ADB-PG向量检索引擎提供专属安全存储,保障企业数据隐私安全。
AnalyticDB PostgreSQL 企业智能数据中台:一站式管理数据服务资产
企业在数据仓库之上可构建丰富的数据服务用以支持数据应用及业务场景;ADB PG推出全新企业智能数据平台,用以帮助用户一站式的管理企业数据服务资产,包括创建, 管理,探索, 监控等; 助力企业在现有平台之上快速构建起数据服务资产体系
相关文章
|
2月前
|
Android开发 Swift iOS开发
iOS和安卓作为主流操作系统,开发者需了解两者差异以提高效率并确保优质用户体验。
【10月更文挑战第1天】随着移动互联网的发展,智能手机成为生活必需品,iOS和安卓作为主流操作系统,各有庞大的用户群。开发者需了解两者差异以提高效率并确保优质用户体验。iOS使用Swift或Objective-C开发,强调简洁直观的设计;安卓则采用Java或Kotlin,注重层次与动画。Swift和Kotlin均有现代编程特性。此外,iOS设备更易优化,而安卓需考虑更多兼容性问题。iOS应用仅能通过App Store发布,审核严格;安卓除Google Play外还可通过第三方市场发布,审核较宽松。开发者应根据需求选择合适平台,提供最佳应用体验。
72 3
|
19天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统架构差异及其对开发者的影响
本文旨在通过对比分析iOS和Android两大移动操作系统的系统架构,探讨它们在设计理念、技术实现及开发者生态方面的差异。不同于常规摘要仅概述内容要点,本摘要将简要触及核心议题,为读者提供对两大平台架构特点的宏观理解,铺垫
|
2月前
|
Android开发
Android开发表情emoji功能开发
本文介绍了一种在Android应用中实现emoji表情功能的方法,通过将图片与表情字符对应,实现在`TextView`中的正常显示。示例代码展示了如何使用自定义适配器加载emoji表情,并在编辑框中输入或删除表情。项目包含完整的源码结构,可作为开发参考。视频演示和源码详情见文章内链接。
74 4
Android开发表情emoji功能开发
|
2月前
|
安全 Android开发 iOS开发
Android vs iOS:探索移动操作系统的设计与功能差异###
【10月更文挑战第20天】 本文深入分析了Android和iOS两个主流移动操作系统在设计哲学、用户体验、技术架构等方面的显著差异。通过对比,揭示了这两种系统各自的独特优势与局限性,并探讨了它们如何塑造了我们的数字生活方式。无论你是开发者还是普通用户,理解这些差异都有助于更好地选择和使用你的移动设备。 ###
54 3
|
27天前
|
安全 Java Linux
深入解析Android系统架构及其对开发者的意义####
【10月更文挑战第21天】 本文旨在为读者揭开Android操作系统架构的神秘面纱,探讨其如何塑造现代移动应用开发格局。通过剖析Linux内核、硬件抽象层、运行时环境及应用程序框架等关键组件,揭示Android平台的强大功能与灵活性。文章强调了理解Android架构对于开发者优化应用性能、提升用户体验的重要性,并展望了未来技术趋势下Android的发展方向。 ####
43 0
|
2月前
|
IDE Android开发 iOS开发
探索安卓与iOS系统的技术差异:开发者的视角
本文深入分析了安卓(Android)与苹果iOS两大移动操作系统在技术架构、开发环境、用户体验和市场策略方面的主要差异。通过对比这两种系统的不同特点,旨在为移动应用开发者提供有价值的见解,帮助他们在不同平台上做出更明智的开发决策。
|
3月前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
414 3
|
3月前
|
IDE Java Android开发
安卓与iOS开发环境的差异及其对开发者的影响
在数字时代的浪潮中,移动应用成为人们生活的延伸。两大操作系统——安卓与iOS,如同两座技术高峰,各自占据着半壁江山。本文将探索这两个平台的开发环境差异,并讨论这些差异如何塑造开发者的编程习惯与职业选择。我们将从工具和语言、用户界面设计、系统架构、市场定位以及开发社区和资源五个方面进行比较,旨在为开发者提供一份实用的指南,帮助他们在不断变化的技术世界中,找到适合自己的发展路径。
58 3
|
3月前
|
移动开发 开发工具 Android开发
安卓与iOS开发:平台差异及其对开发者的影响
在移动开发的大潮中,安卓和iOS两大阵营各领风骚。本文将探讨这两个平台的关键差异,包括开发环境、编程语言、用户界面设计、应用分发以及商业模式等方面。通过比较分析,我们旨在为开发者提供一个清晰的指导,帮助他们根据项目需求和个人偏好做出明智的平台选择。同时,文章也将分享一些跨平台开发工具的使用经验,以期最大化开发效率和市场覆盖。
77 1
|
3月前
|
Android开发 开发者
Android平台无纸化同屏如何实现实时录像功能
Android平台无纸化同屏,如果需要本地录像的话,实现难度不大,只要复用之前开发的录像模块的就可以,对我们来说,同屏采集这块,只是数据源不同而已,如果是自采集的其他数据,我们一样可以编码录像。

相关实验场景

更多