前言 手機等智能設備是現代生活中的重要角色,我們會在這些智能設備上做登錄賬戶,設置偏好,拍攝照片,保存聯繫人等日常操作。這些數據耗費了我們很多時間和精力,對我們而言極為重要。 如果我們的設備換代了或者重新安裝了某個應用,之前使用的數據如果能自動保留,那將是非常出色的用戶體驗。而保留數據的第一步則在於 ...
前言
手機等智能設備是現代生活中的重要角色,我們會在這些智能設備上做登錄賬戶,設置偏好,拍攝照片,保存聯繫人等日常操作。這些數據耗費了我們很多時間和精力,對我們而言極為重要。
如果我們的設備換代了或者重新安裝了某個應用,之前使用的數據如果能自動保留,那將是非常出色的用戶體驗。而保留數據的第一步則在於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地址:
選擇備份模式
如果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傳回來,所以判斷應用版本的辦法行不通。而且有的時候應用版本是一致的,只是運營商不一致。
所以需要我們自己實現,大家可以自行思考。先說我之前想到的幾種方案。
-
備份的時候將設備的名稱埋入SP文件,恢復的時候檢查SP文件里的值
-
備份的時候將設備的名稱埋入新的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地址:
手動發起備份
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功能時需要著重考慮的問題。
版本不一致的情況有兩種。
-
現在運行的應用版本比備份時候的版本高,比較常見的場景
-
現在運行的應用版本比備份時候的版本低,即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功能的開發策略,強化用戶的數據安全。給大家一些實用建議。
-
廠商針對Backup功能的Transport擴展可以是Google雲盤也可以是國內伺服器,App開發者需要關註自己的備份需求和安全策略
-
思考App是否支持備份,明確開關allowBackup屬性
-
更為推薦空間更大、定製靈活的自動備份模式
-
儘快適配Android 12封堵數據泄露的風險
-
隱私級別很高的數據可以補充設備加密的備份條件在備份階段攔截
-
覆寫BackupAgent可以加入恢復的限制,靈活控制流程,在恢復階段二次攔截
Demo地址:
-
提供了鍵值對備份模式的實現
-
針對自動備份模式預設了備份規則,並定製了限製備份源的恢復流程
尾言
最後,希望喜歡本文或者是本文對你有幫助的朋友不妨點個贊,點個關註,你的支持是我更新的最大動力!!!