前言
手機等智能裝置是現代生活中的重要角色,我們會在這些智能裝置上做登入賬戶,設定偏好,拍攝照片,儲存聯系人等日常操作。這些資料耗費了我們很多時間和精力,對我們而言極為重要。
如果我們的裝置換代了或者重新安裝了某個應用,之前使用的資料如果能自動保留,那将是非常出色的使用者體驗。而保留資料的第一步則在于Backup環節。
基本認識
備份的資料可以籠統地劃分為三類:登入賬号相關的身份資料、系統設定相關的偏好以及各App的資料。本次讨論的對象在于App資料。
而App資料基本涵蓋在如下類型。
Backup操作從最外層的data目錄開始,按照檔案機關逐個讀取逐個備份。目錄内的檔案一般按照檔案名的順序進行備份,但這個順序無法保證,取決于File#list() API的結果。Android 6.0之前Backup功能隻有鍵值對備份(Key-value Backup)這一種模式,而且預設是關閉的。想要打開鍵值對備份功能得将allowBackup屬性設定為true,并指定BackupAgent實作。
6.0之後allowBackup屬性預設為true,但是新引入的自動備份(Auto Backup)。自動備份模式執行全體備份和恢複,便捷夠用更推薦。
兩個模式在備份的頻次、檔案的存放位置、恢複的執行時機等細節都很不一樣,下面将針對兩種模式展開實戰示範。
實戰
準備工作
思考Backup的需求
在定制所需的Backup功能前,先了解清楚自己的Backup需求,比如嘗試問自己如下幾個問題。
- 備份的資料Size會很大嗎?超過5M甚至25M嗎?
- 應用的資料全部都需要備份嗎?
- 如果資料很大,需要對應用的部分資料做出取舍,哪些資料可以舍棄?
- 如果恢複的資料的版本不同,能直接恢複嗎?該怎麼定制?
- 定制後的資料能保證繼續讀寫嗎?
準備測試Demo
我們先做個涉及到Data、File、DB以及SP這四種類型資料的App,後面針對這個Demo進行各種Backup功能的定制示範。
Demo通過Jetpack Hilt完成依賴注入,寫入資料的邏輯簡述如下:
- 首次打開的時候尚未産生資料,點選Init Button後會将預設的電影海報儲存到Data目錄,電影Bean執行個體序列化到File目錄,同時通過Jetpack Room将該執行個體儲存到DB。如果三個操作成功執行将初始化成功的Flag标記到SP檔案
- 再次打開的時候依據SP的Flag将會直接讀取這四種類型的資料反映到UI上
Demo位址:
https://github.com/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傳回來,是以判斷應用版本的辦法行不通。而且有的時候應用版本是一緻的,隻是營運商不一緻。
是以需要我們自己實作,大家可以自行思考。先說我之前想到的幾種方案。
- 備份的時候将裝置的名稱埋入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位址:
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功能時需要着重考慮的問題。
版本不一緻的情況有兩種。
- 現在運作的應用版本比備份時候的版本高,比較常見的場景
- 現在運作的應用版本比備份時候的版本低,即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位址:
https://github.com/ellisonchan/BackupRestoreApp
- 提供了鍵值對備份模式的實作
- 針對自動備份模式預設了備份規則,并定制了限制備份源的恢複流程
尾言
最後,希望喜歡本文或者是本文對你有幫助的朋友不妨點個贊,點個關注,你的支援是我更新的最大動力!!!