天天看點

一篇文章帶你全面讀懂Android Backup

一篇文章帶你全面讀懂Android Backup

前言

手機等智能裝置是現代生活中的重要角色,我們會在這些智能裝置上做登入賬戶,設定偏好,拍攝照片,儲存聯系人等日常操作。這些資料耗費了我們很多時間和精力,對我們而言極為重要。

如果我們的裝置換代了或者重新安裝了某個應用,之前使用的資料如果能自動保留,那将是非常出色的使用者體驗。而保留資料的第一步則在于Backup環節。

基本認識

備份的資料可以籠統地劃分為三類:登入賬号相關的身份資料、系統設定相關的偏好以及各App的資料。本次讨論的對象在于App資料。

一篇文章帶你全面讀懂Android Backup

而App資料基本涵蓋在如下類型。

一篇文章帶你全面讀懂Android Backup

Backup操作從最外層的data目錄開始,按照檔案機關逐個讀取逐個備份。目錄内的檔案一般按照檔案名的順序進行備份,但這個順序無法保證,取決于File#list() API的結果。Android 6.0之前Backup功能隻有鍵值對備份(Key-value Backup)這一種模式,而且預設是關閉的。想要打開鍵值對備份功能得将allowBackup屬性設定為true,并指定BackupAgent實作。

一篇文章帶你全面讀懂Android Backup

6.0之後allowBackup屬性預設為true,但是新引入的自動備份(Auto Backup)。自動備份模式執行全體備份和恢複,便捷夠用更推薦。

兩個模式在備份的頻次、檔案的存放位置、恢複的執行時機等細節都很不一樣,下面将針對兩種模式展開實戰示範。

一篇文章帶你全面讀懂Android 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
一篇文章帶你全面讀懂Android Backup
一篇文章帶你全面讀懂Android Backup

選擇備份模式

如果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  
           
一篇文章帶你全面讀懂Android Backup

簡單的備份規則

通過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确實沒有備份成功。

一篇文章帶你全面讀懂Android Backup

補充規則所需的條件

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



           

如果裝置未設定密碼,運作下備份和恢複的指令可以看到圖檔确實也被沒有備份。

一篇文章帶你全面讀懂Android Backup

可是設定了密碼,而且打開了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裝置。

一篇文章帶你全面讀懂Android Backup
如何實作?

因為自動備份模式下不會将資料的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之後确實沒有任何資料。

一篇文章帶你全面讀懂Android Backup

當然如果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目錄都失敗了。這也驗證了上述的結論。

一篇文章帶你全面讀懂Android Backup

因為出于備份檔案空間的考慮,官方并不建議針對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  



           
一篇文章帶你全面讀懂Android Backup

手動發起恢複

除了bmgr工具提供的restore以外還可以通過代碼手動觸發恢複。但這并不安全會影響應用的資料一緻性,是以恢複的API requestRestore()廢棄了。

我們來驗證下,在Demo的Restore Button裡添加BackupManager#requestRestore()的調用。

class LocalData @Inject constructor(...  
                                    val backupManager: BackupManager){  
    fun restoreData() {  
        backupManager.requestRestore(object: RestoreObserver() {  
            ...  
        })  
    }  
    ...  
}  



           

但點選Button之後等一段時間,恢複的日志沒有出現,反倒是彈出了無效的警告。

BackupRestoreApp: LocalData#restoreData()  
BackupManager: requestRestore(): Since Android P app can no longer request restoring of its backup.  



           

備份版本不一緻的處理

版本不一緻意味着恢複之後的邏輯可能會受到影響,這是我們在定制Backup功能時需要着重考慮的問題。

版本不一緻的情況有兩種。

  1. 現在運作的應用版本比備份時候的版本高,比較常見的場景
  2. 現在運作的應用版本比備份時候的版本低,即App降級,不太常見

預設情況下系統會無視App降級的恢複操作,意味着BackupAgent#onRestore()永遠不會被回調。

但如果應用對于舊版本資料的相容處理比較完善,希望支援降級的情況。那麼需要在Manifest裡打開restoreAnyVersion屬性,系統将意識到你的相容并包并回調你的onRestore處理。

無論哪種情況都可以在BackupAgent#onRestore()回調裡拿到備份時的版本。然後讀取App目前的VersionCode,執行對應的資料遷移或丢棄處理。

class MyBackupAgent: BackupAgentHelper() {  
    ...  
    override fun onRestore(  
        data: BackupDataInput?,  
        appVersionCode: Int,  
        newState: ParcelFileDescriptor?  
    ) {  
        val packageInfo = packageManager.getPackageInfo(packageName, 0)  
        if (packageInfo.versionCode != appVersionCode) {  
            // Do something.  
            // 可以調用BackupDataInput#restoreEntity()  
            // 或skipEntityData()決定恢複還是丢棄  
        } else {  
            super.onRestore(data, appVersionCode, newState)  
        }  
    }  
}  



           

直接擴充BackupAgent

擴充自BackupAgent的需要考慮諸多細節,對這個方案有興趣的朋友可以參考BackupAgentHelper的源碼,也可以查閱官方說明。

系統App的Backup限制

部分系統App的隐私級别較高,即便手動調用了Backup指令,系統仍将無視。并在日志中給出提示。

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



           

這個限制的源碼在AppBackupUtils中,解決辦法很簡單在Manifest檔案裡明确指定BackupAgent。

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

實戰總結

Backup定制的總結

當我們遇到Backup定制任務的時候認真思考下需求再對症下藥。為使得這個流程更加直覺,做了個流程圖分享給大家。

一篇文章帶你全面讀懂Android Backup

Backup相關屬性

一篇文章帶你全面讀懂Android Backup

結語

針對Backup功能的持續改善足以瞥見這個功能的重要性。開發者需要對這些改善保持關注,不斷調整Backup功能的開發政策,強化使用者的資料安全。給大家一些實用建議。

  1. 廠商針對Backup功能的Transport擴充可以是Google雲盤也可以是國内伺服器,App開發者需要關注自己的備份需求和安全政策
  2. 思考App是否支援備份,明确開關allowBackup屬性
  3. 更為推薦空間更大、定制靈活的自動備份模式
  4. 盡快适配Android 12封堵資料洩露的風險
  5. 隐私級别很高的資料可以補充裝置加密的備份條件在備份階段攔截
  6. 複寫BackupAgent可以加入恢複的限制,靈活控制流程,在恢複階段二次攔截

Demo位址:

https://github.com/ellisonchan/BackupRestoreApp
  • 提供了鍵值對備份模式的實作
  • 針對自動備份模式預設了備份規則,并定制了限制備份源的恢複流程

尾言

最後,希望喜歡本文或者是本文對你有幫助的朋友不妨點個贊,點個關注,你的支援是我更新的最大動力!!!