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

前言

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

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

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

尾言

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理