关于android:一篇文章带你全面读懂Android-Backup

247次阅读

共计 16675 个字符,预计需要花费 42 分钟才能阅读完成。

前言

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

如果咱们的设施换代了或者重新安装了某个利用,之前应用的数据如果能主动保留,那将是十分杰出的用户体验。而保留数据的第一步则在于 Backup 环节。

根本意识

备份的数据能够抽象地划分为三类:登录账号相干的身份数据、零碎设置相干的偏好以及各 App 的数据。本次探讨的对象在于 App 数据。

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

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

6.0 之后 allowBackup 属性默认为 true,然而新引入的主动备份 (Auto 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/ellisoncha…

抉择备份模式

如果 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" ... />  
</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  

简略的备份规定

通过 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" />  
    <include domain\="sharedpref" path\="." />  
  
    <!-- exclude 指定不参加备份的文件 -->  
    <!-- path 里指定. 代表该目录下所有文件都实用这个规定,免去一一指定各个文件 -->  
    <exclude domain\="file" path\="." />      
    <exclude domain\="database" path\="." />      
</full-backup-content\>  


运行下备份和复原的命令能够看到如下 File 和 DB 的确没有备份胜利。

补充规定所需的条件

当某些隐衷水平极高的数据,不释怀被备份在网络里,但如果数据被加密的话能够思考。面对这种有条件的备份,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\>  


如果设施未设置明码,运行下备份和复原的命令能够看到图片的确也被没有备份。

可是设置了明码,而且关上了 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 设施。

如何实现?

因为主动备份模式下不会将数据的 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 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 之后的确没有任何数据。

当然如果 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" />  
    </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 目录都失败了。这也验证了上述的论断。

因为出于备份文件空间的思考,官网并不倡议针对 DB 文件等大容量文件做键值对备份。实践上能够扩大 FileBackupHelper 对 Data 和 DB 文件做出反对。但 Google 将要害的备份实现(FileBackupHelperBase 和 performBackup\_checked())对外暗藏,使得简略扩大变得不可能。

StackOverFlow 上针对这个问题有过热烈的探讨,惟一的方法是齐全本人实现,但随着主动备份的呈现,这个问题仿佛曾经不再重要。

Demo 地址:

https://stackoverflow.com/que…

手动发动备份

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  


手动发动复原

除了 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.  
            // 能够调用 BackupDataInput#restoreEntity()  
            // 或 skipEntityData() 决定复原还是抛弃} 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.★提醒该 App 不适宜备份操作  
BackupManagerService: Adb backup processing complete.  
BackupManagerService: Full backup pass complete.  


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

其实 Google 的用意很分明,这些零碎级别的 App 数据要是被窃取将非常危险,默认禁止这个操作。但如果你指定了 Backup 代理那代表开发者思考到了备份和复原的场景,对这个操作进行了默认,备份操作才会被放行。

实战总结

Backup 定制的总结

当咱们遇到 Backup 定制工作的时候认真思考下需要再隔靴搔痒。为使得这个流程更加直观,做了个流程图分享给大家。

Backup 相干属性

结语

针对 Backup 性能的继续改善足以瞥见这个性能的重要性。开发者须要对这些改善放弃关注,一直调整 Backup 性能的开发策略,强化用户的数据安全。给大家一些实用倡议。

  1. 厂商针对 Backup 性能的 Transport 扩大能够是 Google 云盘也能够是国内服务器,App 开发者须要关注本人的备份需要和安全策略
  2. 思考 App 是否反对备份,明确开关 allowBackup 属性
  3. 更为举荐空间更大、定制灵便的主动备份模式
  4. 尽快适配 Android 12 封堵数据泄露的危险
  5. 隐衷级别很高的数据能够补充设施加密的备份条件在备份阶段拦挡
  6. 复写 BackupAgent 能够退出复原的限度,灵便管制流程,在复原阶段二次拦挡

Demo 地址:

https://github.com/ellisoncha…

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

尾言

最初,心愿喜爱本文或者是本文对你有帮忙的敌人无妨点个赞,点个关注,你的反对是我更新的最大能源!!!

正文完
 0