针对Backup功能的前作足足三万字,立足点比较大,本篇专门针对功能的实战环节进行解读。

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

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

一、基本认识

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

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

类型 路径 取得对应文件的API
data /data/data/com.xxx/ getDataDir()/getDir()
files /data/data/com.xxx/files/ getFilesDir()
databases /data/data/com.xxx/databases/ getDatabasePath()
sharedpreferences /data/data/com.xxx/sp/ getSharedPreferences()

Backup操作从最外层的data目录开始,按照文件单位逐个读取逐个备份。目录内的文件一般按照文件名的顺序进行备份,但这个顺序无法保证,取决于File#list() API的结果。

#mermaid-svg-OqvvQpfZ5VlEe6JW .label{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);fill:#333;color:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .label text{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .node rect,#mermaid-svg-OqvvQpfZ5VlEe6JW .node circle,#mermaid-svg-OqvvQpfZ5VlEe6JW .node ellipse,#mermaid-svg-OqvvQpfZ5VlEe6JW .node polygon,#mermaid-svg-OqvvQpfZ5VlEe6JW .node path{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-OqvvQpfZ5VlEe6JW .node .label{text-align:center;fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .node.clickable{cursor:pointer}#mermaid-svg-OqvvQpfZ5VlEe6JW .arrowheadPath{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-svg-OqvvQpfZ5VlEe6JW .flowchart-link{stroke:#333;fill:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .edgeLabel{background-color:#e8e8e8;text-align:center}#mermaid-svg-OqvvQpfZ5VlEe6JW .edgeLabel rect{opacity:0.9}#mermaid-svg-OqvvQpfZ5VlEe6JW .edgeLabel span{color:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .cluster rect{fill:#ffffde;stroke:#aa3;stroke-width:1px}#mermaid-svg-OqvvQpfZ5VlEe6JW .cluster text{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-svg-OqvvQpfZ5VlEe6JW .actor{stroke:#ccf;fill:#ECECFF}#mermaid-svg-OqvvQpfZ5VlEe6JW text.actor>tspan{fill:#000;stroke:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .actor-line{stroke:grey}#mermaid-svg-OqvvQpfZ5VlEe6JW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .messageLine1{stroke-width:1.5;stroke-dasharray:2, 2;stroke:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW #arrowhead path{fill:#333;stroke:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .sequenceNumber{fill:#fff}#mermaid-svg-OqvvQpfZ5VlEe6JW #sequencenumber{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW #crosshead path{fill:#333;stroke:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .messageText{fill:#333;stroke:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .labelBox{stroke:#ccf;fill:#ECECFF}#mermaid-svg-OqvvQpfZ5VlEe6JW .labelText,#mermaid-svg-OqvvQpfZ5VlEe6JW .labelText>tspan{fill:#000;stroke:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .loopText,#mermaid-svg-OqvvQpfZ5VlEe6JW .loopText>tspan{fill:#000;stroke:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .loopLine{stroke-width:2px;stroke-dasharray:2, 2;stroke:#ccf;fill:#ccf}#mermaid-svg-OqvvQpfZ5VlEe6JW .note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-OqvvQpfZ5VlEe6JW .noteText,#mermaid-svg-OqvvQpfZ5VlEe6JW .noteText>tspan{fill:#000;stroke:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .activation0{fill:#f4f4f4;stroke:#666}#mermaid-svg-OqvvQpfZ5VlEe6JW .activation1{fill:#f4f4f4;stroke:#666}#mermaid-svg-OqvvQpfZ5VlEe6JW .activation2{fill:#f4f4f4;stroke:#666}#mermaid-svg-OqvvQpfZ5VlEe6JW .mermaid-main-font{font-family:"trebuchet ms", verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .section{stroke:none;opacity:0.2}#mermaid-svg-OqvvQpfZ5VlEe6JW .section0{fill:rgba(102,102,255,0.49)}#mermaid-svg-OqvvQpfZ5VlEe6JW .section2{fill:#fff400}#mermaid-svg-OqvvQpfZ5VlEe6JW .section1,#mermaid-svg-OqvvQpfZ5VlEe6JW .section3{fill:#fff;opacity:0.2}#mermaid-svg-OqvvQpfZ5VlEe6JW .sectionTitle0{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .sectionTitle1{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .sectionTitle2{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .sectionTitle3{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .sectionTitle{text-anchor:start;font-size:11px;text-height:14px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .grid .tick{stroke:#d3d3d3;opacity:0.8;shape-rendering:crispEdges}#mermaid-svg-OqvvQpfZ5VlEe6JW .grid .tick text{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .grid path{stroke-width:0}#mermaid-svg-OqvvQpfZ5VlEe6JW .today{fill:none;stroke:red;stroke-width:2px}#mermaid-svg-OqvvQpfZ5VlEe6JW .task{stroke-width:2}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText{text-anchor:middle;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText:not([font-size]){font-size:11px}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-svg-OqvvQpfZ5VlEe6JW .task.clickable{cursor:pointer}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText0,#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText1,#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText2,#mermaid-svg-OqvvQpfZ5VlEe6JW .taskText3{fill:#fff}#mermaid-svg-OqvvQpfZ5VlEe6JW .task0,#mermaid-svg-OqvvQpfZ5VlEe6JW .task1,#mermaid-svg-OqvvQpfZ5VlEe6JW .task2,#mermaid-svg-OqvvQpfZ5VlEe6JW .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutside0,#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutside2{fill:#000}#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutside1,#mermaid-svg-OqvvQpfZ5VlEe6JW .taskTextOutside3{fill:#000}#mermaid-svg-OqvvQpfZ5VlEe6JW .active0,#mermaid-svg-OqvvQpfZ5VlEe6JW .active1,#mermaid-svg-OqvvQpfZ5VlEe6JW .active2,#mermaid-svg-OqvvQpfZ5VlEe6JW .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-svg-OqvvQpfZ5VlEe6JW .activeText0,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeText1,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeText2,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeText3{fill:#000 !important}#mermaid-svg-OqvvQpfZ5VlEe6JW .done0,#mermaid-svg-OqvvQpfZ5VlEe6JW .done1,#mermaid-svg-OqvvQpfZ5VlEe6JW .done2,#mermaid-svg-OqvvQpfZ5VlEe6JW .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-svg-OqvvQpfZ5VlEe6JW .doneText0,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneText1,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneText2,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneText3{fill:#000 !important}#mermaid-svg-OqvvQpfZ5VlEe6JW .crit0,#mermaid-svg-OqvvQpfZ5VlEe6JW .crit1,#mermaid-svg-OqvvQpfZ5VlEe6JW .crit2,#mermaid-svg-OqvvQpfZ5VlEe6JW .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCrit0,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCrit1,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCrit2,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCrit0,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCrit1,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCrit2,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-svg-OqvvQpfZ5VlEe6JW .milestone{transform:rotate(45deg) scale(0.8, 0.8)}#mermaid-svg-OqvvQpfZ5VlEe6JW .milestoneText{font-style:italic}#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCritText0,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCritText1,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCritText2,#mermaid-svg-OqvvQpfZ5VlEe6JW .doneCritText3{fill:#000 !important}#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCritText0,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCritText1,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCritText2,#mermaid-svg-OqvvQpfZ5VlEe6JW .activeCritText3{fill:#000 !important}#mermaid-svg-OqvvQpfZ5VlEe6JW .titleText{text-anchor:middle;font-size:18px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW g.classGroup text{fill:#9370db;stroke:none;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:10px}#mermaid-svg-OqvvQpfZ5VlEe6JW g.classGroup text .title{font-weight:bolder}#mermaid-svg-OqvvQpfZ5VlEe6JW g.clickable{cursor:pointer}#mermaid-svg-OqvvQpfZ5VlEe6JW g.classGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-OqvvQpfZ5VlEe6JW g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5}#mermaid-svg-OqvvQpfZ5VlEe6JW .classLabel .label{fill:#9370db;font-size:10px}#mermaid-svg-OqvvQpfZ5VlEe6JW .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .dashed-line{stroke-dasharray:3}#mermaid-svg-OqvvQpfZ5VlEe6JW #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #compositionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #aggregationStart{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #aggregationEnd{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #dependencyStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #dependencyEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW #extensionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW .commit-id,#mermaid-svg-OqvvQpfZ5VlEe6JW .commit-msg,#mermaid-svg-OqvvQpfZ5VlEe6JW .branch-label{fill:lightgrey;color:lightgrey;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .pieTitleText{text-anchor:middle;font-size:25px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .slice{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW g.stateGroup text{fill:#9370db;stroke:none;font-size:10px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW g.stateGroup text{fill:#9370db;fill:#333;stroke:none;font-size:10px}#mermaid-svg-OqvvQpfZ5VlEe6JW g.statediagram-cluster .cluster-label text{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW g.stateGroup .state-title{font-weight:bolder;fill:#000}#mermaid-svg-OqvvQpfZ5VlEe6JW g.stateGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-OqvvQpfZ5VlEe6JW g.stateGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-OqvvQpfZ5VlEe6JW .transition{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-OqvvQpfZ5VlEe6JW .stateGroup .composit{fill:white;border-bottom:1px}#mermaid-svg-OqvvQpfZ5VlEe6JW .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px}#mermaid-svg-OqvvQpfZ5VlEe6JW .state-note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-OqvvQpfZ5VlEe6JW .state-note text{fill:black;stroke:none;font-size:10px}#mermaid-svg-OqvvQpfZ5VlEe6JW .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.7}#mermaid-svg-OqvvQpfZ5VlEe6JW .edgeLabel text{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .stateLabel text{fill:#000;font-size:10px;font-weight:bold;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-OqvvQpfZ5VlEe6JW .node circle.state-start{fill:black;stroke:black}#mermaid-svg-OqvvQpfZ5VlEe6JW .node circle.state-end{fill:black;stroke:white;stroke-width:1.5}#mermaid-svg-OqvvQpfZ5VlEe6JW #statediagram-barbEnd{fill:#9370db}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-cluster rect{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-cluster rect.outer{rx:5px;ry:5px}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-state .divider{stroke:#9370db}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-state .title-state{rx:5px;ry:5px}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-cluster.statediagram-cluster .inner{fill:white}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-cluster .inner{rx:0;ry:0}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-state rect.basic{rx:5px;ry:5px}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}#mermaid-svg-OqvvQpfZ5VlEe6JW .note-edge{stroke-dasharray:5}#mermaid-svg-OqvvQpfZ5VlEe6JW .statediagram-note rect{fill:#fff5ad;stroke:#aa3;stroke-width:1px;rx:0;ry:0}:root{--mermaid-font-family: '"trebuchet ms", verdana, arial';--mermaid-font-family: "Comic Sans MS", "Comic Sans", cursive}#mermaid-svg-OqvvQpfZ5VlEe6JW .error-icon{fill:#522}#mermaid-svg-OqvvQpfZ5VlEe6JW .error-text{fill:#522;stroke:#522}#mermaid-svg-OqvvQpfZ5VlEe6JW .edge-thickness-normal{stroke-width:2px}#mermaid-svg-OqvvQpfZ5VlEe6JW .edge-thickness-thick{stroke-width:3.5px}#mermaid-svg-OqvvQpfZ5VlEe6JW .edge-pattern-solid{stroke-dasharray:0}#mermaid-svg-OqvvQpfZ5VlEe6JW .edge-pattern-dashed{stroke-dasharray:3}#mermaid-svg-OqvvQpfZ5VlEe6JW .edge-pattern-dotted{stroke-dasharray:2}#mermaid-svg-OqvvQpfZ5VlEe6JW .marker{fill:#333}#mermaid-svg-OqvvQpfZ5VlEe6JW .marker.cross{stroke:#333}:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}#mermaid-svg-OqvvQpfZ5VlEe6JW {color: rgba(0, 0, 0, 0.75);font: ;}

data
files
databases
sharedpreferences

Android 6.0之前Backup功能只有键值对备份(Key-value Backup)这一种模式,而且默认是关闭的。想要打开键值对备份功能得将allowBackup属性设置为true,并指定BackupAgent实现。

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

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

备份模式 键值对备份 自动备份
支持版本 Android 2.2 Android 6.0
开关办法 默认关闭,需手动开启allowBackup并指定BackupAgent 默认开启,关闭需要将allowBackup置为false
备份定制 BackupAgent里指定备份和恢复的文件 可以通过XML配置备份和不备份的文件列表,也可以通过BackupAgent改写备份恢复的逻辑,定制性高
备份时机 需要App调用API手动发起备份 自动进行,大约每天一次
备份的托管位置 Android Backup Service/Google服务器 Google Drive云盘
备份限制 上限只有5M 上限有25M
恢复时机 APK安装的时候自动恢复,也可以调用API手动发起恢复 APK安装的时候自动恢复
原理细节 回调到BackupAgent的onBackup()和onRestore() 回调到BackupAgent的onFullBackup()和onRestoreFile()

二、实战

Ⅰ. 准备工作

ⅰ. 思考Backup的需求

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

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

ⅱ. 准备测试Demo

我们先做个涉及到DataFileDB以及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


Ⅱ. 选择备份模式

如果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将关键的备份实现(FileBackupHelperBaseperformBackup_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: 3BackupRequest{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的源码,也可以查阅官方说明。
https://developer.android.google.cn/guide/topics/data/keyvaluebackup

Ⅴ. 系统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相关属性

相关属性 说明
allowBackup 是否支持Backup,默认为true
backupAgent 指定Backup代理进行定制
fullBackupContent 指定备份规则XML文件
restoreAnyVersion 是否支持高版本数据恢复到低版本应用,默认为false
fullBackupOnly 在指定了BackupAgent后仍然采用AutoBackup模式
killAfterRestore 全系统恢复期后是否终止应用,默认为 true
backupInForeground 即使应用处于前台也可以对其执行自动备份,默认为false
clientSideEncryption 只在手机设置密钥的情况下执行备份
deviceToDeviceTransfer 只在D2D设备间备份的情况下执行备份

三、结语

Android 12 Beta版公开在即,针对Backup功能又做了些改善。主要体现在两个方面,一是将备份规则针对云端备份和设备到设备备份两种场景区分开来,更加合理;二是加大adb backup命令的限制,对Backup功能可能造成的数据泄露进行了封堵。

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

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

四、DEMO

https://github.com/ellisonchan/BackupRestoreApp

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

参考资料

备份功能的官方主页
键值对备份模式
自动备份模式

推荐阅读

全面复盘Android开发者容易忽视的Backup功能
Jetpack Hilt有哪些改善又有哪些限制?
除了SQLite一定要试试Room

Android Backup功能之全面实战相关推荐

  1. android远程打电话,Android打电话功能 Android实战教程第三篇之简单实现拨打电话功能...

    想了解Android实战教程第三篇之简单实现拨打电话功能的相关内容吗,杨道龙在本文为您仔细讲解Android打电话功能的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:Android拨打电话 ...

  2. Android多功能时钟源代码,Android多功能时钟开发案例(实战篇)

    上一篇为大家介绍的是Android多功能时钟开发基础内容,大家可以回顾一下,Android多功能时钟开发案例(基础篇) 接下来进入实战,快点来学习吧. 一.时钟 在布局文件中我们看到,界面上只有一个T ...

  3. android开发入门与实践_我的新书《Android App开发入门与实战》已经出版

    前言 工作之余喜欢在CSDN平台上写一些技术文章,算下时间也有两三年了.写文章的目的一方面是自己对技术的总结,另一方面也是将平时遇到的问题和解决方案与大家分享,还有就是在这个平台上能和大家共同交流. ...

  4. Android 2.3应用开发实战

    Android 2.3应用开发实战全面介绍Android OS操作环境,包括SDK 2.3版最新内容 丰富的范例程序详解Android应用开发,便于读者快速掌握 每一章的主题都安排进阶学习,满足不同读 ...

  5. Android 语音播报之项目实战

    TextToSpeech项目应用 文字转语音 官网简介 项目前景 项目实战 真机调试 结尾 文字转语音 从文本合成语音以立即播放或创建声音文件.即TextToSpeech(以下简称TTS)       ...

  6. 《Android 网络开发与应用实战详解》——1.3节搭建Android应用开发环境

    本节书摘来自异步社区<Android 网络开发与应用实战详解>一书中的第1章,第1.3节搭建Android应用开发环境,作者 王东华,更多章节内容可以访问云栖社区"异步社区&qu ...

  7. 《Android 网络开发与应用实战详解》——2.3节Android系统架构

    本节书摘来自异步社区<Android 网络开发与应用实战详解>一书中的第2章,第2.3节Android系统架构,作者 王东华,更多章节内容可以访问云栖社区"异步社区"公 ...

  8. 《Android 网络开发与应用实战详解》——2.1节简析Android安装文件

    本节书摘来自异步社区<Android 网络开发与应用实战详解>一书中的第2章,第2.1节简析Android安装文件,作者 王东华,更多章节内容可以访问云栖社区"异步社区" ...

  9. 我的新书《Android App开发入门与实战》已经出版

    文章目录 1. 前言 2. 写书的目的 3. 书籍简介 4. 书籍目标读者群体 5. 书籍比较 6. 书籍特色 7. 书籍章节 8. 书籍封面 9. 购书地址 10. 本书案例及源码下载 1. 前言 ...

  10. android4 设置栈大小,【技术分享】Android内核漏洞利用技术实战:环境搭建栈溢出实战...

    [技术分享]Android内核漏洞利用技术实战:环境搭建&栈溢出实战 2017-08-14 16:22:02 阅读:0次 预估稿费:300RMB 投稿方式:发送邮件至linwei#360.cn ...

最新文章

  1. Zabbix-03-3 告警变量值
  2. 理解Kalman滤波的使用
  3. 大话设计模式读书笔记2----单一职责原则(SRP)
  4. 超声相控阵合成孔径成像FPGA设计介绍
  5. Python快速简单生成矩形词云
  6. 再见了古诺。 你好Drools工作台。
  7. 【小技巧】字符char与整型int的相互转换
  8. OSS音频编程概述(DSP部分)
  9. 如何优雅的追到女神夕小瑶
  10. 指针做形参,形参的传递详解
  11. ECCV 2020 论文大盘点-视频目标检测篇
  12. 问题:使用pandas中的DataFrame写入csv文件多出一行unnamed,如何解决呢??
  13. win10计算机盘符如何,删除win10电脑多余无需使用的盘符教程
  14. Tomcat文件包含漏洞:CNVD-2020-10487(简介/验证/利用/修复)
  15. 台式电脑一般价钱多少_一般普通台式电脑价格多少为好?
  16. 保护FTP和SFTP服务器的10个基本技巧
  17. 解决WinBUGS14 error:cannot bracket slice for node gamma[3]
  18. 后台系统的权限控制与管理
  19. USB OTG连接方式
  20. 微信小程序 输入车牌号(有新能源)

热门文章

  1. 关于手画猫,耳朵涂颜色
  2. js(javascript)和jq(jquery)常见问题(持续更新)
  3. 利用吉洪若夫正则化及其西尔韦斯特方程来修复受损图像
  4. 怎样把计算机网络共享给手机,怎么把电脑网络共享给手机
  5. 改变文本颜色和字体大小的脚本
  6. 6sigma 基本概念
  7. ISO9000、CMM(I)、6sigma与对象分析技术
  8. C++类内初始值的初始化形式
  9. DOS批处理文件加密文件夹
  10. 用JS实现万年历效果,精!