一篇文章带你全面读懂Android Backup

一篇文章带你全面读懂Android Backup

前言

手机等智能设备是现代生活中的重要角色,我们会在这些智能设备上做登录账户,设置偏好,拍摄照片,保存联系人等日常操作。这些数据耗费了我们很多时间和精力,对我们而言极为重要。

如果我们的设备换代了或者重新安装了某个应用,之前使用的数据如果能自动保留,那将是非常出色的用户体验。而保留数据的第一步则在于Backup环节。

基本认识

备份的数据可以笼统地划分为三类:登录账号相关的身份数据、系统设置相关的偏好以及各App的数据。本次讨论的对象在于App数据。

一篇文章带你全面读懂Android Backup

而App数据基本涵盖在如下类型。

一篇文章带你全面读懂Android Backup

Backup操作从最外层的data目录开始,按照文件单位逐个读取逐个备份。目录内的文件一般按照文件名的顺序进行备份,但这个顺序无法保证,取决于File#list() API的结果。Android 6.0之前Backup功能只有键值对备份(Key-value Backup)这一种模式,而且默认是关闭的。想要打开键值对备份功能得将allowBackup属性设置为true,并指定BackupAgent实现。

一篇文章带你全面读懂Android Backup

6.0之后allowBackup属性默认为true,但是新引入的自动备份(Auto Backup)。自动备份模式执行全体备份和恢复,便捷够用更推荐。

两个模式在备份的频次、文件的存放位置、恢复的执行时机等细节都很不一样,下面将针对两种模式展开实战演示。

一篇文章带你全面读懂Android Backup

实战

准备工作

思考Backup的需求

在定制所需的Backup功能前,先了解清楚自己的Backup需求,比如尝试问自己如下几个问题。

  • 备份的数据Size会很大吗?超过5M甚至25M吗?
  • 应用的数据全部都需要备份吗?
  • 如果数据很大,需要对应用的部分数据做出取舍,哪些数据可以舍弃?
  • 如果恢复的数据的版本不同,能直接恢复吗?该怎么定制?
  • 定制后的数据能保证继续读写吗?

准备测试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

一篇文章带你全面读懂Android Backup

一篇文章带你全面读懂Android Backup

选择备份模式

如果Backup需求不复杂,那优先选择自动备份模式。因为这个模式提供的空间更大、定制也更灵活。是Google首推的Backup模式。如果应用数据Size很小而且愿意手动实现DB文件的备份恢复逻辑的话,可以采用键值对备份模式。

自动备份

鉴于键值对备份的诸多不足,Google在6.0推出的自动备份模式带来了很多改善。

  • 自动执行无需手动发起
  • 更大的备份空间(由原来的5M变成了25M)
  • 更多类型文件的支持(在File和SP文件以外还支持了Data和DB文件)
  • 更简单的备份规则(通过XML即可快速指定备份对象)
  • 更安全的备份条件(在规则中指定flag可限定备份执行的条件)

基本定制

想要支持自动备份模式的话,什么代码也不用写,因为6.0开始自动备份模式默认打开。但我还是推荐开发者明确地打开allowBackup属性,这表示你确实意识到Backup功能并决定支持它。


<manifest ...>
    <application android:allowbackup\="true" ...>

</application></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

一篇文章带你全面读懂Android Backup

简单的备份规则

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

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


<manifest ...>
    <application android:allowbackup\="true" android:fullbackupcontent\="@xml/my\_backup\_rules" ...>

</application></manifest>

<!-- my\_backup\_rules.xml -->
<full-backup-content\>
    <!-- include指定参与备份的文件 -->
    <!-- domain指定root代表这个的规则适用于data目录 -->
    <include domain\="root" path\="Post.jpg">
    <include domain\="sharedpref" path\=".">

    <!-- exclude指定不参与备份的文件 -->
    <!-- path里指定.代表该目录下所有文件都适用这个规则,免去逐个指定各个文件 -->
    <exclude domain\="file" path\=".">
    <exclude domain\="database" path\=".">
</exclude></exclude></include></include></full-backup-content\>

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

一篇文章带你全面读懂Android Backup

补充规则所需的条件

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

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

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

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


<!-- my\_backup\_rules.xml -->
<full-backup-content\>
    <include domain\="root" path\="Post.jpg" requireflags\="clientSideEncryption">
    ...

</include></full-backup-content\>

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

一篇文章带你全面读懂Android Backup

可是设置了密码,而且打开了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" ...>

</application></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()&#x3000;
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() &#x2605;
BackupManagerService: Adb backup processing complete.

BackupRestoreAgent: onDestroy()
AndroidRuntime: Shutting down VM
BackupManagerService: Full backup pass complete. &#x2605;

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

进阶定制之限制备份来源

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

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

一篇文章带你全面读懂Android Backup
如何实现?

因为自动备份模式下不会将数据的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?) {
        // &#x2605; &#x5728;&#x5907;&#x4EFD;&#x6267;&#x884C;&#x524D;&#x5148;&#x5C06;&#x6807;&#x8BB0;&#x6587;&#x4EF6;&#x5199;&#x5165;Data&#x76EE;&#x5F55;
        // 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)
            // &#x2605; &#x5907;&#x4EFD;&#x6E90;&#x8BBE;&#x5907;&#x540D;&#x548C;&#x5F53;&#x524D;&#x540D;&#x4E0D;&#x4E00;&#x81F4;&#x7684;&#x65F6;&#x5019;&#x6807;&#x8BB0;&#x9700;&#x8981;&#x8DF3;&#x8FC7;
            // 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 {
            // &#x2605; &#x8DF3;&#x8FC7;&#x5907;&#x4EFD;&#x4F46;&#x4E00;&#x5B9A;&#x8981;&#x6D88;&#x8D39;stream&#x9632;&#x6B62;&#x6062;&#x590D;&#x7684;&#x8FDB;&#x7A0B;&#x963B;&#x585E;
            // 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()
        // &#x79FB;&#x9664;&#x6807;&#x8BB0;&#x6587;&#x4EF6;
        // 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()
//  &#x2605;&#x6807;&#x8BB0;&#x6587;&#x4EF6;&#x91CC;&#x5199;&#x5165;&#x4E86;&#x5C0F;&#x7C73;&#x7684;&#x8BBE;&#x5907;&#x540D;&#x79F0;&#x5E76;&#x5907;&#x4EFD;&#x4E86;
BackupRestoreAgent: writeBackupSourceToFile() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A create:true &#x2605;
BackupRestoreAgent: onDestroy()
BackupManagerService: Adb backup processing complete.

BackupRestoreAgent: ensureBackupSourceFileRemoved() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A delete:true &#x2605;
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
// &#x2605;&#x4ECE;&#x5907;&#x4EFD;&#x6570;&#x636E;&#x91CC;&#x8BFB;&#x53D6;&#x5230;&#x4E86;&#x5C0F;&#x7C73;&#x7684;&#x8BBE;&#x5907;&#x540D;&#xFF0C;&#x4E0D;&#x540C;&#x4E8E;Pixel&#x6A21;&#x62DF;&#x5668;&#x7684;&#x540D;&#x79F0;&#xFF0C;&#x8BBE;&#x5B9A;&#x4E86;&#x8DF3;&#x8FC7;&#x6062;&#x590D;&#x7684;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 &#x2605;
...

BackupRestoreAgent: onRestoreFinished()
BackupManagerService: \[UserID:0\] adb restore processing complete.

BackupRestoreAgent: onDestroy()
BackupManagerService: Full restore pass complete.

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

一篇文章带你全面读懂Android Backup

当然如果App确实有在根目录下存放数据,那么建议你仍采用这个方案。

只不过需要给这个特定文件加一个a的前缀,以保证它大多数情况下会被先恢复到。当然为了防止极低的概率下它没有首先被恢复,开发者还需自行加上一个Data目录下文件的暂存和回退处理,以防万一。

更高的定制需求

如果发现备份的设备名称不一致的时候,客户的需求并不是丢弃恢复,而是让我们将运营商之间的diff merge进来呢?

这里提供一个思路。在上述方案的基础之上改下就行了。

比如恢复的一开始通过标记的文件发现备份的不一致,丢弃恢复的同时将待恢复的文件都改个别名暂存到本地。应用再次打开的时候读取暂存的数据和当前数据做对比,然后将diff merge进来。

如果不是限制恢复而是怕恢复的数据被别人看到,需要加个验证保护,怎么做?

譬如在恢复数据结束之后存一个需要验证账号的Flag。当App打开的时候发现Flag的存在会强制验证账户,输入验证码等。

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")
            }
        }
    }
}

键值对备份

键值对备份支持的空间小,而且针对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">

</meta-data></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&#x548C;DB&#x6587;&#x4EF6;&#x4F7F;&#x7528;FileBackupHelper&#x662F;&#x65E0;&#x6CD5;&#x5907;&#x4EFD;&#x7684;&#xFF0C;&#x6B64;&#x5904;&#x5355;&#x7EAF;&#x4E3A;&#x4E86;&#x9A8C;&#x8BC1;&#x4E0B;
        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&#x548C;SP&#x5404;&#x81EA;&#x4F7F;&#x7528;&#x5BF9;&#x5E94;&#x7684;Helper&#x662F;&#x53EF;&#x4EE5;&#x5907;&#x4EFD;&#x7684;
        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。从日志可以看出来键值对备份和恢复成功进行了。


// &#x5F00;&#x542F;bmgr&#x548C;&#x8BBE;&#x7F6E;&#x672C;&#x5730;&#x4F20;&#x8F93;&#x670D;&#x52A1;
\>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

// &#x6E05;&#x7A7A;&#x6570;&#x636E;
\>adb shell pm clear com.ellison.backupdemo

// &#x67E5;&#x770B;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目录都失败了。这也验证了上述的结论。

一篇文章带你全面读懂Android Backup

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

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

Demo地址:

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} &#x2605;
    ...

我们可以强制这个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() &#x2605;
BackupRestoreAgent: onDestroy()
BackupManagerService: \[UserID:0\] Released wakelock:\*backup\*\-0-1265

一篇文章带你全面读懂Android Backup

手动发起恢复

除了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功能时需要着重考虑的问题。

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

  1. 现在运行的应用版本比备份时候的版本高,比较常见的场景
  2. 现在运行的应用版本比备份时候的版本低,即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.

            // &#x53EF;&#x4EE5;&#x8C03;&#x7528;BackupDataInput#restoreEntity()
            // &#x6216;skipEntityData()&#x51B3;&#x5B9A;&#x6062;&#x590D;&#x8FD8;&#x662F;&#x4E22;&#x5F03;
        } else {
            super.onRestore(data, appVersionCode, newState)
        }
    }
}

直接扩展BackupAgent

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

系统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.&#x2605;&#x63D0;&#x793A;&#x8BE5;App&#x4E0D;&#x9002;&#x5408;&#x5907;&#x4EFD;&#x64CD;&#x4F5C;
BackupManagerService: Adb backup processing complete.

BackupManagerService: Full backup pass complete.

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

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

实战总结

Backup定制的总结

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

一篇文章带你全面读懂Android Backup

Backup相关属性

一篇文章带你全面读懂Android Backup

结语

针对Backup功能的持续改善足以瞥见这个功能的重要性。开发者需要对这些改善保持关注,不断调整Backup功能的开发策略,强化用户的数据安全。给大家一些实用建议。

  1. 厂商针对Backup功能的Transport扩展可以是Google云盘也可以是国内服务器,App开发者需要关注自己的备份需求和安全策略
  2. 思考App是否支持备份,明确开关allowBackup属性
  3. 更为推荐空间更大、定制灵活的自动备份模式
  4. 尽快适配Android 12封堵数据泄露的风险
  5. 隐私级别很高的数据可以补充设备加密的备份条件在备份阶段拦截
  6. 复写BackupAgent可以加入恢复的限制,灵活控制流程,在恢复阶段二次拦截

Demo地址:

https://github.com/ellisonchan/BackupRestoreApp

  • 提供了键值对备份模式的实现
  • 针对自动备份模式预设了备份规则,并定制了限制备份源的恢复流程

尾言

最后,希望喜欢本文或者是本文对你有帮助的朋友不妨点个赞,点个关注,你的支持是我更新的最大动力!!!

Original: https://www.cnblogs.com/BlueSocks/p/16175347.html
Author: BlueSocks
Title: 一篇文章带你全面读懂Android Backup

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/607427/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Python 排序算法之快速排序

    快速排序之分治法三步走 """ 快速排序 分治法(divide and conquer),三步走 1. Partition:选择一个基准(pivot)…

    Linux 2023年6月13日
    090
  • 经典45个git使用技巧与场合,专治不会合代码。

    前言 git对于大家应该都不太陌生,熟练使用git已经成为程序员的一项基本技能,尽管在工作中有诸如 Sourcetree这样牛X的客户端工具,使得合并代码变的很方便。但找工作面试和…

    Linux 2023年6月13日
    0118
  • RNN循环神经网络

    1.为什么还会有RNN? CNN(卷积神经网络)我们会发现, 他们的输出都是只考虑前一个输入的影响而不考虑其它时刻输入的影响, 比如简单的猫,狗,手写数字等单个物体的识别具有较好的…

    Linux 2023年6月6日
    0123
  • 剑指offer计划28(搜索与回溯算法困难)—java

    1.1、题目1 剑指 Offer 37. 序列化二叉树 1.2、解法 这题给我笑死了,我看到题解有个解法,我愿称之为神。 public class Codec { private …

    Linux 2023年6月11日
    088
  • 多线程执行同一任务,不共享局部变量

    多线程执行同一任务,不共享局部变量 一、 代码展示 import threading import time 多线程执行同一任务时,局部变量是不共享的 def sum_num():…

    Linux 2023年6月14日
    083
  • springboot 工程出现 socket hang up

    出现socket hang up 原因为程序处理时间长,超出了默认请求超时时间,导致socket断开 可以通过设置请求超时时间降低出现的概率以下示例中设置请求超时时间为三分钟 sp…

    Linux 2023年6月8日
    094
  • windows系统如何查看端口被占用、杀进程

    查看系统当前所有的端口使用情况 命令:netstat -ano 查看特定端口是否被占用: netstat -ano |findstr “端口号” 查看到对应…

    Linux 2023年6月13日
    097
  • Tomcat

    Tomcat Tomcat tomcat简介 tomcat的用处 部署tomcat 测试访问 访问Host Manager界面 访问Server Status tomcat简介 T…

    Linux 2023年6月6日
    0111
  • 07-MyBatis中的动态标签

    MyBatis中的动态标签 1、if标签 if标签是为了判断传入的值是否符合某种条件,比如是否不为空 2、where标签 where标签可以用来做动态拼接查询条件,当和 if标签配…

    Linux 2023年6月7日
    087
  • shell之常用的一些命令

    前言 自我感觉学习shell要常用man,–help这样挺方便去写的,光靠熟练远远不够的 bash;gutter:false;</p> <h1>!/bin…

    Linux 2023年6月8日
    081
  • 综合布线 子网掩码 IP地址 子网划分

    A类地址:1.0.0.0~126.255.255.255 255.0.0.0 B类地址:128.0.0.0~191.255.255.255 255.255.0.0 C类地址:192…

    Linux 2023年6月6日
    072
  • linux编译安装nginx

    本文升级过程,适用于大部分nginx编译版本 常用编译选项说明nginx大部分常用模块,编译时./configure –help以–without开头的都默认安装。 –prefix…

    Linux 2023年6月14日
    082
  • Fabric2.2中的Raft共识模块源码分析

    引言 Hyperledger Fabric是当前比较流行的一种联盟链系统,它隶属于Linux基金会在2015年创建的超级账本项目且是这个项目最重要的一个子项目。目前,与Hyperl…

    Linux 2023年6月7日
    068
  • Python之元类详解

    一、引子 元类属于Python面向对象编程的深层魔法,99%的人都不得要领,一些自以为搞明白元类的人其实也是自圆其说,点到为止,从队元类的控制上来看就破绽百出,逻辑混乱; 二、什么…

    Linux 2023年6月14日
    084
  • C语言传指针类型的形参

    今天在牛客网上做C语言专项练习题,遇到一个”函数传指针类型的形参”的题,我做错了,正确的为下面代码: #include <string.h> #…

    Linux 2023年6月13日
    064
  • Mysql数据库 ALTER 基本操作

    背景: ALTER作为DDL语言之一,工作中经常遇到,这里我们简单介绍一下常见的几种使用场景 新建两个测试表offices 和 employess CREATE TABLE off…

    Linux 2023年6月6日
    0101
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球