主頁 > 移動端開發 > 全面復盤Android開發者容易忽視的Backup功能

全面復盤Android開發者容易忽視的Backup功能

2021-04-27 18:10:13 移動端開發

在這里插入圖片描述

allowBackup屬性大家都不陌生,為了安全起見最好將它關閉,對它的認識好像也僅限于此了,事實上Google在Backup功能上花了很多心思,提供了多個模式的選擇和充分的定制介面,本文將帶大家全面復盤Backup功能的由來原理定制方法,以備不時之需,篇幅較長,配合收藏更佳,

為節省部分讀者的時間,貼出重點章節供快速空降,

  • 關心Backup功能原理的,直接空降2.8章節
  • 關心Backup功能測驗的,直接空降3.2章節
  • 關心Backup檔案破解的,直接空降3.3章節
  • 關心Backup實戰的,直接空降4.0章節
  • 關心Android 12影響的,直接空降5.0章節

1. 前言

兩年前我就遇到過一個Backup功能相關的CTS問題,說的是整機恢復到AccessibilitySerivce的時候發生錯誤,整機備份和恢復非常耗時,我不可能真的跑一遍去定位問題,我得找個高效的辦法,

通過查閱Backup的原理我知道了可以單獨BackupRestore某個app,甚至可以解密備份檔案查看資料內容,有了這些方法的協助,我很快就找到了原因,

雖然很快地解決了那個問題,但我的心里留下了一個想法,Backup功能好像比我想象的復雜有趣,一定要找個時間好好了解一下,恰逢近期在做Backup功能的定制,對這塊有了充分的認識,便整理出來分享給大家,

2. 完整認識Backup

2.1 功能由來

手機等智能設備是現代生活中的重要角色,我們會在這些智能設備上做登錄賬戶,設定偏好,拍攝照片,保存聯系人等日常操作,

這些資料耗費了我們很多時間和精力,對我們而言極為重要,如果我們的設備換代了或者重新安裝了某個應用,之前使用的資料如果能自動保留,那將是非常出色的用戶體驗,而保留資料的第一步則在于Backup環節,

2.2 資料來源

用戶的資料可以籠統地劃分為三塊:登錄賬號相關的身份資料、系統設定相關的偏好以及各App的資料,這三塊資料的型別不同、位置不同,進而導致Backup的實作也不同,
cmd-markdown-logo

  • App資料:應用內部的圖片,視頻等資料,這是我們尤為關心的資料,如何安全完整地轉移這些資料是Backup功能的目標所在,也是本文需要講解的核心內容
  • 身份資料:用戶登錄的身份資料,可以通過Smart Lock或Account Transfer API在設備間立即恢復登錄狀態
  • 設定偏好:系統設定App和SettingProvider將記錄用戶的偏好資料,甚至包括用戶授予App的權限記錄,系統將針對這些設定資料備份和恢復

2.3 備份物件

我們知道可以將資料存放在App目錄,也可以存放于公共目錄,但隨著Android系統針對公共目錄的限制愈加嚴格,將資料存放到App自己的目錄顯得更加合理,

App自身目錄的這塊資料順理成章地成為Backup功能的主要物件,按照檔案的型別可以細分如下,

型別路徑取得對應檔案的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()

注意:

  • 放置在外部存盤空間中的檔案也是支持的,這里不再贅述
  • cache、nobackup等目錄下的檔案不在Backup物件內

Backup操作從最外層的data目錄開始,按照檔案單位逐個讀取逐個備份,目錄內的檔案一般按照檔案名的順序進行備份,但這個順序無法保證,取決于File#list() API的結果,

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

2.4 如何開啟

在Manifest檔案里使用allowBackup屬性可以控制Backup功能的開關,這個屬性早在Android 1.6(API 4)的時候便引入了,起初默認是關閉的,

allowBackup
Added in API level 4
Whether to allow the application to participate in the backup and restore infrastructure. If this attribute is set to false, no >backup or restore of the application will ever be performed, even by a full-system backup that would otherwise cause all >application data to be saved via adb. The default value of this attribute is true.

可能是Android設備火了,備份恢復的需求越來越大,自Android 6.0之后默認值變成了true,即默認將支持Backup功能,

2.5 開啟的隱患

allowBackup屬性開啟的話,開發者使用幾個簡單的adb命令就可以將應用的資料備份和恢復,意味著自己的資料被轉移到別人的設備上是非常簡單的,這無疑造成了具大的安全隱患,

后面的實戰將會揭曉控制備份和恢復的諸多可能,比如在恢復的邏輯里加入了限制使得惡意的恢復失敗,但即便在恢復階段攔截了,備份的ab檔案仍掌握在別人手里,仍不穩妥,因為通過破解備份檔案也可以閱讀到部分甚至全部內容,

所以涉及到私密資料的App最好將該屬性關閉,自行考慮資料的同步方式,比如通過賬號系統恢復,

這個屬性曾經引發安全隱患,詳情可參考:https://blog.csdn.net/zihao2012/article/details/44220389

2.6 備份模式

Android 6.0之前Backup功能只有鍵值對備份(Key-value Backup)這一種模式,而且默認是關閉的,

想要打開鍵值對備份功能得將allowBackup屬性設定為true,并指定BackupAgent實作,即明確地告知訴Backup功能每個檔案按照什么key備份到Android Backup Service,簡單來講,必須給Backup功能提供一個備份檔案的映射關系,好讓它知道備份的源頭和恢復的目標,

6.0之后allowBackup屬性默認為true,但打開的不是鍵值對備份,而是新引入的自動備份(Auto Backup),自動備份模式執行傻瓜式的全體備份和恢復,可供備份檔案存放的空間更大,便捷夠用更推薦,,

兩個模式在備份的頻次、檔案的存放位置、恢復的執行時機等細節都很不一樣,

備份模式鍵值對備份自動備份
支持版本Android 2.2Android 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()

2.7 備份的托管位置

使用鍵值對模式備份的檔案托管在Google服務器(Android Backup Service),Google承諾將會加密傳輸這些資料,并尊重隱私條款,同時在App關閉Backup功能的時候洗掉這些備份,可以放心使用,

如果采用了默認的自動備份模式,那資料存放在Google Drive云盤,云盤擁有自己的賬號系統,使用的是賬號級別的加密保護,更為安全,

2.8 實作原理

那Android系統是如何實作Backup和Restore功能的呢?

在解答這個問題之前,我們先思考下如果你是Google開發者,你會怎么實作?

這里有個ContentProvider方案,簡言之,使用一個統一調度的App通過ContentProvider組件向各個實作了特定Uri或Permission的ContentProvider App發出讀寫資料的請求,

  • 各App通過ContentProvider將需要備份的Data、File、DB以及SP檔案傳輸出去,調度App收集到包名為單位的備份檔案集合,加密后上傳到服務器或記憶體卡
  • 恢復則是調度App將檔案解密之后通過ContentProvider再回傳給各App,各App自行執行恢復的邏輯

但這個方案有點缺憾,對于大部分App來說檔案全部備份和恢復就行了,不需要搞特別的定制,但實際情況是需要支持備份恢復的話,就得各自實作一套ContentProvider去做檔案的收集和覆寫,

而Google采取的方案是這樣的,默認認為每個App都支持Backup功能,然后給App提供同樣的BackupAgent去執行自動備份和恢復的處理,當App存在特別定制的需求的時候可以指定擴展的BackupAgent邏輯,更加靈活高效,

內部的實作原理簡述如下,

  1. 系統服務BMS(BackupManagerService)收到BackupManager API發起的備份/恢復請求后,該服務將通過IBackupTransport和Cloud端建立連接
  2. BMS通過持有的BackupHandler依據操作引數啟動相應的Backup或Restore執行緒
  3. 任務執行緒通過AppBackupUtils檢查該App是否支持Backup/Restore
  4. 之后依據備份模式創建對應的Engine并通過通過IBackupAgent向App發起實際的操作請求
  5. BackupAgent將按照對應模式去讀取或寫入檔案
    在這里插入圖片描述

3. 發起、除錯和解密Backup

3.1 Backup/Restore的發起

3.1.1 代碼方式

選取了鍵值對備份模式的話,需要在資料發生變動的時候手動發起備份,SDK提供了API:BackupManager的dataChanged(),呼叫之后BackupManager將調度備份請求在適當的時機發起備份處理,

事實上BackupManager還提供了requestRestore()供我們手動發起恢復,API的回傳值將告訴我們是否將要執行恢復操作,這個API發起的恢復結束之后不會KILL行程,存在造成資料錯亂的隱患,最好依賴于系統自行的恢復操作,
※自Android 9.0開始這個API廢棄了,呼叫了也沒有反應,

3.1.2 命令方式

ⅰ. adb命令

adb的backup和restore命令可以幫助我們手動發起較為簡單的請求,但只支持自動備份模式,

  • Backup
// 比如備份某個App的資料并以指定的名稱保存備份檔案
adb backup -f <fileName>.ab -apk <packageName>

接下來系統會提示我們輸入備份密碼,
在這里插入圖片描述
輸完密碼之后點擊開始備份,系統將彈出備份開始或結束的Toast,當然不輸入密碼直接備份也是可以的,但備份的資料容易被破解,

  • Restore
// 發起恢復請求的命令很簡單
adb restore <fileName>.ab

接下來輸入密碼開始恢復,同樣的會有Toast提示恢復的進度,
在這里插入圖片描述

ⅱ.bmgr工具

adb backup命令提供的功能不夠強大,官方推薦bmgr工具,它將備份和恢復的步驟分得更細,便于我們理清各個環節,更好的協助我們測驗備份和恢復的邏輯,

bmgr工具沒有UI,完全通過命令在后臺默默運行,

首先需要啟用它,
注意:要確保設定里的Backup功能沒有被關閉,Settings > Backup & Restore,

>adb shell bmgr enabled
Backup Manager currently enabled

接著,查看ROM里支持的檔案傳輸服務,*號表示當前選擇的服務,

>adb shell bmgr list transports 
    com.android.localtransport/.LocalTransport
    com.google.android.gms/.backup.migrate.service.D2dTransport
  * com.google.android.gms/.backup.BackupTransportService

GMS的傳輸服務要求設備聯網和科學上網,為方面測驗我們切換服務為本地傳輸

>adb shell bmgr transport com.android.localtransport/.LocalTransport
Selected transport com.android.localtransport/.LocalTransport (formerly com.google.android.gms/.backup.BackupTransportService)

查看傳輸服務的更改是否生效,

>adb shell bmgr list transports
  * com.android.localtransport/.LocalTransport
    com.google.android.gms/.backup.migrate.service.D2dTransport
    com.google.android.gms/.backup.BackupTransportService

針對某個App發起備份,

>adb shell bmgr backupnow <package>

在另一個終端捕捉備份的執行日志,有可能會提示沒有設定鎖屏密碼

Backup  : [CryptoEnableCheck] Should not encrypt backups: device has no lock screen.

設定密碼后再次發起備份,可以看到成功備份了,

>adb shell bmgr backupnow <package>
Package xxx with result: Success
Backup finished with result: Success

日志終端也顯示回呼了App指定的BackupAgent

AndroidRuntime: Calling main entry com.android.commands.bmgr.Bmgr
PFTBT   : backupmanager pftbt token=4081832e
BackupManagerService: awaiting agent for ApplicationInfo{30f779b xxx}
BackupRestoreAgent: MyBackupAgent()
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=xxx agent=android.os.BinderProxy@5f88b66
BackupManagerService: got agent android.app.IBackupAgent$Stub$Proxy@c309ea7
BackupRestoreAgent: onBackup()
BackupRestoreAgent: onDestroy()

bmgr工具在手動恢復的時候需要Token資訊,通過dumpsys backup獲取對應的Token,Token來自于AncestralCurrent兩個標簽的組合,比如本次的Token為01,

>adb shell dumpsys backup
Backup Manager is enabled / setup complete / not pending init
Auto-restore is enabled
No backups running
Last backup pass started: 1619317275335 (now = 1619319671619)
  next scheduled: 1619332172012
...
Ancestral: 0 ★
Current:   1...

清空App資料,

>adb shell pm clear <package>

手動恢復資料,從命令和日志兩個終端都能看到資料被正確恢復了,

>adb shell bmgr restore 01 <package>
Scheduling restore: Local disk image
restoreStarting: 1 packages
onUpdate: 0 = com.example.alldemo
restoreFinished: 0
done
BackupRestoreAgent: MyBackupAgent()
BackupRestoreAgent: onCreate()
BackupManagerService: agentConnected pkg=com.example.alldemo agent=android.os.BinderProxy@a480a0c
BackupManagerService: got agent android.app.IBackupAgent$Stub$Proxy@5041f55
BackupManagerService: initiateOneRestore packageName=xxx
BackupRestoreAgent: onRestore()
BackupManagerService: restoreFinished packageName=xxx
BackupRestoreAgent: onRestoreFinished()
BackupManagerService: Restore complete, killing host process of xxx ★
BackupRestoreAgent: onDestroy()
BackupManagerService: No more packages; finishing restore
BackupManagerService: Restore complete.

當然將App卸載后通過市場或手動安裝可以自動地恢復資料,這個動作由系統在Apk安裝的時候自動完成,

Transport服務的選擇要小心,如果選了GMS Transport的話,要注意GMS場景的網路問題,不然備份會失敗,
更加詳細的bmgr使用方法可參考如下檔案,
https://developer.android.google.cn/studio/command-line/bmgr?hl=zh-cn

3.1.3 Google發起

Google將會按照每日一次的頻次對支持自動備份模式的App發起備份操作,

恢復的話則是在設備第一次開機登錄Google賬號后,Google會將資料從服務器下載通過BackupManager向各個備份過的App發起恢復操作,尚未安裝的App則在后期Apk安裝完成之后由Google自行發起恢復,

3.2 Backup/Restore的除錯

logcat指定BackupManagerService的Tag,可以監聽到Backup和Restore的日志,輔助我們把握操作的進度和報錯的原因,

>adb logcat -s BackupManagerService

比如針對Google Photos App進行adb備份和恢復操作的時候,將會輸出如下日志,

  • Backup
>adb logcat -s BackupManagerService
BackupManagerService: Requesting backup: apks=true obb=false shared=false all=false system=true includekeyvalue=false pkgs=[Ljava.lang.String;@190020e
BackupManagerService: Beginning adb backup...
BackupManagerService: Starting backup confirmation UI, token=1441721864
BackupManagerService: Waiting for backup completion...
BackupManagerService: acknowledgeAdbBackupOrRestore : token=1441721864 allow=true
BackupManagerService: --- Performing adb backup ---
BackupManagerService: Package com.google.android.apps.photos is key-value.
BackupManagerService: Adb backup processing complete.
BackupManagerService: Full backup pass complete.
  • Restore
>adb logcat -s BackupManagerService
BackupManagerService: Beginning restore...
BackupManagerService: Starting restore confirmation UI, token=1694423050
BackupManagerService: Waiting for restore completion...
BackupManagerService: acknowledgeAdbBackupOrRestore : token=1694423050 allow=true
BackupManagerService: --- Performing full-dataset restore ---
BackupManagerService: adb restore processing complete.
BackupManagerService: Full restore pass complete.

一般來說BackupManagerService提供的日志情報足夠了,但在除錯Transport,使用bmgr工具等場景的時候,還可以使用這些Tag獲得更詳細的日志:Backup,BackupManager,PFTBT,GmsBackupTransport,PerformBackupTask和RestoreSession等,

>adb logcat -s AndroidRuntime -s Backup -s BackupManager -s BackupManagerService -s PFTBT -s GmsBackupTransport -s -s PerformBackupTask -s RestoreSession

3.3 Backup檔案的解密

Backup檔案的后綴名為.ab,估計是android backup的縮寫,我們用Text打開上面備份的Google Photos檔案,可以看到如下資訊,

ANDROID BACKUP
5
1
AES-256
C356E772D89C31C0FCAE6BF16BEC2FF90F0503BCD12111B380FF6054B823D80963EEDC661D92DB908788B48499A80B62731C1A9822C8BF5CD8D67AE85FF45CD9
...

整個檔案內容包含頭和內容,其中頭的資訊非常重要,關乎到備份的策略和解密的方式,

  • Backup功能的版本號,比如上面的5,定義在原始碼的UserBackupManagerService檔案中
  • Backup備份檔案是否壓縮,比如上面的1意味著經過了壓縮
  • Backup加密方式,比如上面采用了AES-256加密演算法,如果未輸入密碼備份的話,此處會顯示none

未輸入密碼的ab檔案,

ANDROID BACKUP
5
1
none
xレb
...

我們可以使用abe.jar來解密備份的檔案,如果使用了加密演算法的話,還需要Java Cryptography Extension jar包的幫助,

這里簡單演示下沒有加密的備份檔案的破解程序,

// 輸入如下命令
java -jar abe.jar unpack backupFileName-nopwd.ab backupFileName-nopwd.tar

未輸出任何Exception則表示解密成功,并會生成指定的tar包,解壓出來之后是包括DB、SP在內的原始資料,
在這里插入圖片描述

abe.jar全名為android-backup-extractor,是采用Java語言撰寫的轉為解密Android備份檔案的工具,非常好用,
abe.jar下載地址

除了這個工具,貌似DD命令也可以破解,筆者沒有試過,感興趣的可以參考如下文章進行更深入的嘗試,
淺談安卓系統備份檔案ab格式決議

4. 實戰

鋪墊了關于Backup功能的大量知識,就是想讓完整地認識和理解這個功能,接下來進入最實用的實戰環節,

4.1 準備作業

4.1.1 思考Backup的需求

在定制所需的Backup功能前,先了解清楚自己的Backup需求,比如嘗試問自己如下幾個問題,

  • 備份的資料Size會很大嗎?超過5M甚至25M嗎?
  • 應用的資料全部都需要備份嗎?
  • 如果資料很大,需要對應用的部分資料做出取舍,哪些資料可以舍棄?
  • 如果恢復的資料的版本不同,能直接恢復嗎?該怎么定制?
  • 定制后的資料能保證繼續讀寫嗎?

4.1.2 準備測驗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

在這里插入圖片描述
在這里插入圖片描述

4.2 選擇備份模式

如果Backup需求不復雜,那優先選擇自動備份模式,因為這個模式提供的空間更大、定制也更靈活,是Google首推的Backup模式,

如果應用資料Size很小而且愿意手動實作DB檔案的備份恢復邏輯的話,可以采用鍵值對備份模式,

4.3 自動備份

鑒于鍵值對備份的諸多不足,Google在6.0推出的自動備份模式帶來了很多改善,

  • 自動執行無需手動發起
  • 更大的備份空間(由原來的5M變成了25M)
  • 更多型別檔案的支持(在File和SP檔案以外還支持了Data和DB檔案)
  • 更簡單的備份規則(通過XML即可快速指定備份物件)
  • 更安全的備份條件(在規則中指定flag可限定備份執行的條件)

ⅰ. 基本定制

想要支持自動備份模式的話,什么代碼也不用寫,因為6.0開始自動備份模式默認打開,但我還是推薦開發者明確地打開allowBackup屬性,這表示你確實意識到Backup功能并決定支持它

<manifest ... >
    <application android:allowBackup="true" ... />
</manifest>

開啟之后同樣使用adb命令模擬備份恢復的程序,通過截圖可以看到所有資料都被完整恢復了

// Backup
>adb backup -f auto-backup.ab -apk com.ellison.backupdemo
// Clear data
>adb shell pm clear com.ellison.backupdemo
// Restore
>adb restore auto-backup.ab

在這里插入圖片描述

ⅱ. 簡單的備份規則

通過fullBackupContent屬性可以指向包含備份規則的 XML 檔案,我們可以在規則里決定了備份哪些檔案,無視哪些檔案,

比如只需要備份放在Data的海報圖片和SP,不需要File和DB檔案,

<manifest ... >
    <application android:allowBackup="true"
        android:fullBackupContent="@xml/my_backup_rules" ... />
</manifest>
<!-- my_backup_rules.xml -->
<full-backup-content>
    <!-- include指定參與備份的檔案 -->
    <!-- domain指定root代表這個的規則適用于data目錄 -->
    <include domain="root" path="Post.jpg"/>
    <!-- path里指定.代表該目錄下所有檔案都適用這個規則,免去逐個指定各個檔案 -->
    <include domain="sharedpref" path="."/>

    <!-- exclude指定不參與備份的檔案 -->
    <exclude domain="file" path="."/>
    <exclude domain="database" path="."/>
</full-backup-content>

運行下備份和恢復的命令可以看到如下File和DB確實沒有備份成功,
在這里插入圖片描述

ⅲ.補充規則所需的條件

當某些隱私程度極高的資料,不放心被備份在網路里,但如果資料被加密的話可以考慮,面對這種有條件的備份,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進來

ⅵ.BackupAgent和配置規則的混用

BackupAgent和XML配置并不沖突,在backup邏輯里還可以獲取配置的設備條件,比如在onFullBackup()里可以利用FullBackupDataOutput的getTransportFlags()來取得相應的Flag來執行相應的邏輯,

  • FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED 對應著設備加密條件
  • FLAG_DEVICE_TO_DEVICE_TRANSFER 對應D2D備份場景條件
class MyBackupAgent: BackupAgentHelper() {
    ...
    override fun onFullBackup(data: FullBackupDataOutput?) {
        Log.d(Constants.TAG_BACKUP, "onFullBackup()")
        super.onFullBackup(data)

        if (data != null) {
            if ((data.transportFlags and FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED) != 0) {
                Log.d(Constants.TAG_BACKUP, "onFullBackup() CLIENT ENCRYPTION NEED")
            }
        }
    }
}

4.4 鍵值對備份

鍵值對備份支持的空間小,而且針對File型別的Backup實作非執行緒安全,同時需要自行考慮DB這種大空間檔案的備份處理,并不推薦使用,

但本著學習的目的還是要了解一下,

ⅰ. 基本定制

使用這個模式需額外指定BackupAgent并實作其細節,

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

BackupAgent的實作在于告訴BMS每個型別的檔案采用什么Key備份和恢復,可以選擇高度定制的復雜辦法去實作,當然SDK也提供了簡單辦法,

  • 復雜辦法:直接擴展自BackupAgent抽象類,需要自行實作onBackup()onRestore的細節,包括讀取各型別檔案并呼叫對應的Helper實作寫入資料到備份檔案中以及考慮舊的備份資料的遷移等處理,需要考慮很多細節,代碼量很大
  • 簡單辦法:擴展自系統封裝好的BackupAgentHelper類并告知各型別檔案對應的KEY和Helper實作即可,高效而簡單,但沒有提供大容量檔案比如DB的備份實作

以擴展BackupAgentHelper的簡單辦法為例,演示下鍵值對備份的實作,

  • SP檔案的話SDK提供了特定的SharedPreferencesBackupHelper實作
  • File檔案對應的Helper實作為FileBackupHelper,只限于file目錄的資料
  • 其他型別檔案比如Data和DB是沒有預設Helper實作的,需要自行實作BackupHelper
// MyBackupAgent.kt
class MyBackupAgent: BackupAgentHelper() {
    override fun onCreate() {
        ...
        // Init helper for data, file, db and sp files.
        // Data和DB檔案使用FileBackupHelper是無法備份的,此處單純為了驗證下
        FileBackupHelper(this, Constants.DATA_NAME).also { addHelper(Constants.BACKUP_KEY_DATA, it) }
        FileBackupHelper(this, Constants.DB_NAME).also { addHelper(Constants.BACKUP_KEY_DB, it) }
        // File和SP各自使用對應的Helper是可以備份的
        FileBackupHelper(this, Constants.FILE_NAME).also { addHelper(Constants.BACKUP_KEY_FILE, it) }
        SharedPreferencesBackupHelper(this, Constants.SP_NAME).also { addHelper(Constants.BACKUP_KEY_SP, it) }
    }
    ...
}

先用bmgr工具執行Backup,然后清除Demo的資料再執行Restore,從日志可以看出來鍵值對備份和恢復成功進行了,

// 開啟bmgr和設定本地傳輸服務
>adb shell bmgr enabled
>adb shell bmgr transport com.android.localtransport/.LocalTransport

// Backup
>adb shell bmgr backupnow com.ellison.backupdemo
Running incremental backup for 1 requested packages.
Package @pm@ with result: Success
Package com.ellison.backupdemo with result: Success
Backup finished with result: Success

// 清空資料
>adb shell pm clear com.ellison.backupdemo

// 查看Backup Token
>adb shell dumpsys backup
...
Ancestral: 0
Current:   1

// Restore
>adb shell bmgr restore 01 com.ellison.backupdemo
Scheduling restore: Local disk image
restoreStarting: 1 packages
onUpdate: 0 = com.ellison.backupdemo
restoreFinished: 0
done

Demo的截圖顯示File和SP備份和恢復成功了,但存放在Data目錄的海報和DB目錄都失敗了,這也驗證了上述的結論,
在這里插入圖片描述
因為出于備份檔案空間的考慮,官方并不建議針對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: 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的原始碼,也可以查閱官方說明,
https://developer.android.google.cn/guide/topics/data/keyvaluebackup

4.5 系統App的Backup限制

部分系統App的隱私級別較高,即便手動呼叫了Backup命令,系統仍將無視,并在日志中給出提示,

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

這個限制的原始碼在AppBackupUtils中,解決辦法很簡單在Manifest檔案里明確指定BackupAgent

其實Google的意圖很清楚,這些系統級別的App資料要是被竊取將十分危險,默認禁止這個操作,但如果你指定了Backup代理那代表開發者考慮到了備份和恢復的場景,對這個操作進行了默許,備份操作才會被放行,

4.6 實戰總結

4.6.1 Backup定制的總結

當我們遇到Backup定制任務的時候認真思考下需求再對癥下藥,為使得這個流程更加直觀,做了個流程圖分享給大家,
在這里插入圖片描述

4.6.2 Backup相關屬性

相關屬性說明
allowBackup是否支持Backup,默認為true
backupAgent指定Backup代理進行定制
fullBackupContent指定備份規則XML檔案
restoreAnyVersion是否支持高版本資料恢復到低版本應用,默認為false
fullBackupOnly在指定了BackupAgent后仍然采用AutoBackup模式
killAfterRestore全系統恢復期后是否終止應用,默認為 true
backupInForeground即使應用處于前臺也可以對其執行自動備份,默認為false
clientSideEncryption只在手機設定密鑰的情況下執行備份
deviceToDeviceTransfer只在D2D設備間備份的情況下執行備份

5. Android 12的影響和Backup功能的發展歷程

Android 12 Beta版即將公開,其針對Backup功能又做了些改動,先來看看變更的說明,

5.1 D2D 設備到設備備份的規則細分

For apps running on and targeting Android 12 and higher:

  • Specifying android:allowBackup="false" does disable backups to Google Drive, but doesn’t disable D2D transfers for the app.
  • Specifying include and exclude rules with the XML configuration mechanism no longer affects D2D transfers, though it still affects Google Drive backups. To specify rules for D2D transfers, you must use the new configuration covered in the next section.

簡直之,Android 12開始即便關閉了allowBackup屬性,D2D的Backup功能仍將有效,不再受影響,同時原有的通過fullBackupContent指定的配置規則也將失效,

如果你的App目標版本是Android 12的話,需要使用新屬性dataExtractionRules來指定語法規則,

語法規則的所變化主要體現在使用新的屬性cloud-backupdevice-transfer明示地區分云端備份和D2D備份的規則,而不再像之前那樣采用full-backup-content指定統一的規則,

另外原有的設備條件flag也發生了變化,

  • clientSideEncryption:在新規則里變成了disableIfNoEncryptionCapabilities,且只能應用在cloud-backup標簽內
  • deviceToDeviceTransfer:新規則將D2D區分開來了,所以這個flag不需要了
<application
    android:dataExtractionRules="new_config.xml"
    ...>
</application>
<data-extraction-rules>
  <cloud-backup [disableIfNoEncryptionCapabilities="true|false"]>
    <include domain=["file" | "database" | "sharedpref" | "external" |
                        "root"] path="string"/>
	...
  </cloud-backup>
  <device-transfer>
    <include domain=["file" | "database" | "sharedpref" | "external" |
                        "root"] path="string"/>
    ...
  </device-transfer>
</data-extraction-rules>

原因在于云端備份存在空間的限制,難免需要對備份的檔案做出取舍,而D2D的場景檔案是存在本地的,沒有這種限制了卻還對備份檔案做出削減顯然不太合理,

具體細節可參考官方檔案,
https://developer.android.google.cn/about/versions/12/backup-restore

5.2 adb backup命令的限制

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

adb backup命令是可以備份整機資料的,從Android 12開始該資料里將不包含App部分的應用資料,除非在Manifest里手動打開debuggable屬性,

如果備份單個App也失敗的話,那安全性將大大提高,筆者在12 Preview版本上執行該命令仍舊能夠正常備份,不知道是不是Target SDK的問題,等正式版出來后再嘗試下,

詳情可參考官方說明,
https://developer.android.google.cn/about/versions/12/behavior-changes-12

5.3 Backup功能的發展歷程

簡要回顧下Backup功能的發展歷程,供快速查閱,

版本變化內容
Android 1.6加入allowBackup屬性默認關閉
Android 2.2開始使用鍵值對備份模式
Android 6.0開始支持自動備份模式,默認打開allowBackup屬性
Android 7.0Backup功能將自動備份和恢復用戶授予App的權限
Android 9.0新增了加密存盤備份檔案
Android 12D2D場景的Backup規則變更和adb backup命令的限制

6. 結語

Google針對Backup功能的頻繁改動可以看出來其對于這個功能的重視,總結起來就是在功能的易用性,安全性,合理性之間反復優化,

針對這些變化開發者需要不斷調整Backup功能的開發策略,我也給出一些實用建議,

  1. 思考App是否支持備份,明示地設定allowBackup屬性
  2. 自動備份模式提供的備份空間更大,定制靈活,更為推薦
  3. 隱私級別很高的資料可以添加設備加密的備份條件
  4. 復寫BackupAgent可以靈活定制備份和恢復的流程,值得好好研究
  5. 出于學習和調查的目的可以嘗試了解和破解Backup檔案
  6. backup命令已不推薦,除錯Backup功能盡量嘗試功能更為強大的bmgr工具

未決懸念

  1. 官方檔案說明鍵值對備份從2.2開始提供支持,可是allowBackup屬性自1.6便于匯入,那在2.2之前的備份采取哪種模式呢?
    想找到一臺2.2以前的設備去驗證動作不太現實,打算在2.2之前的系統原始碼里找到答案,
  2. Android 12上目標SDK為12的話如果debuggable未開的話,無論備份整機還是單個app都將失敗?
  3. clientSideEncryption表示Backup功能打開且設定了密碼均可開始執行備份,但實際測驗不是,總是沒有執行備份,

DEMO

https://github.com/ellisonchan/BackupRestoreApp

參考資料

備份功能的官方主頁
鍵值對備份模式
自動備份模式
測驗備份和恢復
bmgr工具
allowBackup造成的安全問題
Backup檔案解密JAR包
Backup檔案的決議
鍵值對備份模式的DB支持
Android 12的行為變更

推薦閱讀

Jetpack Hilt有哪些改善又有哪些限制?
Dagger2和它在SystemUI上的應用
除了SQLite一定要試試Room

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/280725.html

標籤:其他

上一篇:vue3.0 + vite (一)頁面渲染

下一篇:Android_Google Play結算庫(應用內支付)billing 3.0接入實戰

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more