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

来源:https://www.cnblogs.com/BlueSocks/archive/2022/04/21/16175347.html
-Advertisement-
Play Games

前言 手機等智能設備是現代生活中的重要角色,我們會在這些智能設備上做登錄賬戶,設置偏好,拍攝照片,保存聯繫人等日常操作。這些數據耗費了我們很多時間和精力,對我們而言極為重要。 如果我們的設備換代了或者重新安裝了某個應用,之前使用的數據如果能自動保留,那將是非常出色的用戶體驗。而保留數據的第一步則在於 ...


前言

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

如果我們的設備換代了或者重新安裝了某個應用,之前使用的數據如果能自動保留,那將是非常出色的用戶體驗。而保留數據的第一步則在於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傳回來,所以判斷應用版本的辦法行不通。而且有的時候應用版本是一致的,只是運營商不一致。

所以需要我們自己實現,大家可以自行思考。先說我之前想到的幾種方案。

  1. 備份的時候將設備的名稱埋入SP文件,恢復的時候檢查SP文件里的值

  2. 備份的時候將設備的名稱埋入新的File文件,恢復的時候檢查File文件的值

這倆方案的缺陷:方案1的缺點在於備份的邏輯會在原有的文件里增加值,會影響現有的邏輯。

方案2增加了新文件,避免對現有的邏輯造成影響,對方案1有所改善。但它和方案1都存在一個潛在的問題。

問題在於無法保證這個新文件首先被恢復到,也就無保證在恢復執行的一開始就知道本次恢復是否需要。

假使恢復進行到了一半,輪到標記新文件的時候才發現本次恢復需要丟棄,那麼將會導致數據錯亂。因為系統沒有提供Roll back已恢複數據的API,如果我們自己也沒做好保存和回退舊的文件處理的話,最後必然發生部分文件已恢復部分沒恢復的不一致問題。

要理解這個問題就要搞清楚恢復操作針對文件的執行順序。

自動備份模式在恢復的時候會逐個調用onRestoreFile(),將各個目錄下備份的文件回調過來。目錄之間的順序和備份時候的順序一致,如下備份的代碼可以看出來:從根目錄的Data開始,接著File目錄開始,然後DB和SP文件。



public abstract class BackupAgent extends ContextWrapper {  
    ...  
    public void onFullBackup(FullBackupDataOutput data) throws IOException {  
        ...  
        // Root dir first.  
        applyXmlFiltersAndDoFullBackupForDomain(  
                packageName, FullBackup.ROOT\_TREE\_TOKEN, manifestIncludeMap,  
                manifestExcludeSet, traversalExcludeSet, data);  
        // Data dir next.  
        traversalExcludeSet.remove(filesDir);  
        // Database directory.  
        traversalExcludeSet.remove(databaseDir);  
        // SharedPrefs.  
        traversalExcludeSet.remove(sharedPrefsDir);  
    }  
}  



文件內的順序則通過File#list()獲取,而這個API是無法保證得到的文件列表都按照abcd的字母排序。所以在File目錄下放標記文件不能保證它首先被恢復到。即便放一個a開頭的標記文件也不能完全保證。

推薦方案

一般的App鮮少在根目錄存放數據,而根目錄最先被恢復到。所以我推薦的方案是這樣的。

備份的時候將設備的名稱埋入根目錄的特定文件,恢復的時候檢查該File文件,在恢復的初期就決定本次恢復是否需要。為了不影響恢復之後的正常使用,最後還要刪除這個標記文件。

廢話不多說,看下代碼。

Backup里放入標記文件



class MyBackupAgent : BackupAgentHelper() {  
    ...  
    override fun onFullBackup(data: FullBackupDataOutput?) {  
        // ★ 在備份執行前先將標記文件寫入Data目錄  
        // Make backup source file before full backup invoke.  
        writeBackupSourceToFile()  
        super.onFullBackup(data)  
    }  
  
    private fun writeBackupSourceToFile() {  
        val sourceFile = File(dataDir.absolutePath + File.separator  
                + Constants.BACKUP\_SOURCE\_FILE\_PREFIX + Build.MODEL)  
        if (!sourceFile.exists()) {  
            sourceFile.createNewFile()  
        }  
    }  
    ...  
}  



Restore檢查標記文件



class MyBackupAgent : BackupAgentHelper() {  
    private var needSkipRestore = false  
    ...  
    override fun onRestoreFile(  
            data: ParcelFileDescriptor?,  
            size: Long,  
            destination: File?,  
            type: Int,  
            mode: Long,  
            mtime: Long  
    ) {  
        if (!needSkipRestore) {  
            val sourceDevice = readBackupSourceFromFile(destination)  
            // ★ 備份源設備名和當前名不一致的時候標記需要跳過  
            // Mark need skip restore if source got and not match current device.  
            if (!TextUtils.isEmpty(sourceDevice) && !sourceDevice.equals(Build.MODEL)) {  
                needSkipRestore = true   
            }  
        }  
  
        if (!needSkipRestore) {  
            // Invoke restore if skip flag set.  
            super.onRestoreFile(data, size, destination, type, mode, mtime)  
        } else {  
            // ★ 跳過備份但一定要消費stream防止恢復的進程阻塞  
            // Consume data to keep restore stream go.  
            consumeData(data!!, size, type, mode, mtime, null)   
        }  
    }  
    ...  
    private fun readBackupSourceFromFile(file: File?): String {  
        if (file == null) return ""  
        var decodeDeviceSource = ""  
  
        // Got data file with backup source mark.  
        if (file.name.startsWith(Constants.BACKUP\_SOURCE\_FILE\_PREFIX)) {  
            decodeDeviceSource = file.name.replace(Constants.BACKUP\_SOURCE\_FILE\_PREFIX, "")  
        }  
        return decodeDeviceSource  
    }  
  
    @Throws(IOException::class)  
    fun consumeData(data: ParcelFileDescriptor,  
                    size: Long, type: Int, mode: Long, mtime: Long, outFile: File?) {  
        ...  
    }  
}  



無論是Backup還是Restore都要將標記文件移除



class MyBackupAgent : BackupAgentHelper() {  
    ...  
    override fun onDestroy() {  
        super.onDestroy()  
        // 移除標記文件  
        // Ensure temp source file is removed after backup or restore finished.  
        ensureBackupSourceFileRemoved()  
    }  
  
    private fun ensureBackupSourceFileRemoved() {  
        val sourceFile = File(dataDir.absolutePath + File.separator  
                + Constants.BACKUP\_SOURCE\_FILE\_PREFIX + Build.MODEL)  
        if (sourceFile.exists()) {  
            val result = sourceFile.delete()  
        }  
    }  
}  



接下里驗證代碼能否攔截不同設備的備份文件。先在小米手機里備份文件,然後到Pixel模擬器里恢復這個數據。

在小米手機里備份



\>adb -s c7a1a50c7d27 backup -f auto-backup-cus-xiaomi.ab -apk com.ellison.backupdemo  
  
\>adb -s c7a1a50c7d27 logcat -s BackupManagerService -s BackupRestoreAgent  
BackupManagerService: --- Performing full backup for package com.ellison.backupdemo ---  
BackupRestoreAgent: onCreate()  
BackupManagerService: agentConnected pkg=com.ellison.backupdemo agent=android.os.BinderProxy@5e68506  
BackupManagerService: got agent android.app.IBackupAgent$Stub$Proxy@852a7c7  
BackupManagerService: Calling doFullBackup() on com.ellison.backupdemo  
BackupRestoreAgent: onFullBackup()  
//  ★標記文件里寫入了小米的設備名稱並備份了  
BackupRestoreAgent: writeBackupSourceToFile() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A create:true ★  
BackupRestoreAgent: onDestroy()  
BackupManagerService: Adb backup processing complete.  
BackupRestoreAgent: ensureBackupSourceFileRemoved() sourceFile:/data/user/0/com.ellison.backupdemo/backup-source-Redmi 6A delete:true ★  
BackupManagerService: Full backup pass complete.  



往Pixel手機里恢復,可以看到Pixel的日誌里顯示跳過了恢復。



\>adb -s emulator\-5554 restore auto-backup-cus-xiaomi.ab  
  
\>adb -s emulator\-5554 logcat -s BackupManagerService -s BackupRestoreAgent  
BackupManagerService: --- Performing full-dataset restore ---  
...  
BackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/backup-source-Redmi 6A type:1  mode:384  mtime:1619355877 currentDevice:sdk\_gphone\_x86\_arm needSkipRestore:false  
BackupRestoreAgent: readBackupSourceFromFile() file:/data/data/com.ellison.backupdemo/backup-source-Redmi 6A  
BackupRestoreAgent: readBackupSourceFromFile() source:Redmi 6A  
BackupRestoreAgent: onRestoreFile() sourceDevice:Redmi 6A  
// ★從備份數據里讀取到了小米的設備名,不同於Pixel模擬器的名稱,設定了跳過恢復的flag  
BackupRestoreAgent: onRestoreFile() destination:/data/data/com.ellison.backupdemo/Post.jpg type:1  mode:384  mtime:1619355781 currentDevice:sdk\_gphone\_x86\_arm needSkipRestore:true  
BackupRestoreAgent: onRestoreFile() skip restore and consume ★  
...  
BackupRestoreAgent: onRestoreFinished()  
BackupManagerService: \[UserID:0\] adb restore processing complete.  
BackupRestoreAgent: onDestroy()  
BackupManagerService: Full restore pass complete.  



Pixel模擬器上重新打開App之後確實沒有任何數據。

當然如果App確實有在根目錄下存放數據,那麼建議你仍採用這個方案。

只不過需要給這個特定文件加一個a的首碼,以保證它大多數情況下會被先恢復到。當然為了防止極低的概率下它沒有首先被恢復,開發者還需自行加上一個Data目錄下文件的暫存和回退處理,以防萬一。

更高的定製需求

如果發現備份的設備名稱不一致的時候,客戶的需求並不是丟棄恢復,而是讓我們將運營商之間的diff merge進來呢?

這裡提供一個思路。在上述方案的基礎之上改下就行了。

比如恢復的一開始通過標記的文件發現備份的不一致,丟棄恢復的同時將待恢復的文件都改個別名暫存到本地。應用再次打開的時候讀取暫存的數據和當前數據做對比,然後將diff merge進來。

如果不是限制恢復而是怕恢復的數據被別人看到,需要加個驗證保護,怎麼做?

譬如在恢複數據結束之後存一個需要驗證賬號的Flag。當App打開的時候發現Flag的存在會強制驗證賬戶,輸入驗證碼等。

BackupAgent和配置規則的混用

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

  • FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED 對應著設備加密條件

  • FLAG_DEVICE_TO_DEVICE_TRANSFER 對應D2D備份場景條件



class MyBackupAgent: BackupAgentHelper() {  
    ...  
    override fun onFullBackup(data: FullBackupDataOutput?) {  
        Log.d(Constants.TAG\_BACKUP, "onFullBackup()")  
        super.onFullBackup(data)  
  
        if (data != null) {  
            if ((data.transportFlags and FLAG\_CLIENT\_SIDE\_ENCRYPTION\_ENABLED) != 0) {  
                Log.d(Constants.TAG\_BACKUP, "onFullBackup() CLIENT ENCRYPTION NEED")  
            }  
        }  
    }  
}  



鍵值對備份

鍵值對備份支持的空間小,而且針對File類型的Backup實現非線程安全,同時需要自行考慮DB這種大空間文件的備份處理,並不推薦使用。

但本著學習的目的還是要瞭解一下。

基本定製

使用這個模式需額外指定BackupAgent並實現其細節。



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



BackupAgent的實現在於告訴BMS每個類型的文件採用什麼Key備份和恢復。可以選擇高度定製的複雜辦法去實現,當然SDK也提供了簡單辦法。

  • 複雜辦法:直接擴展自BackupAgent抽象類,需要自行實現onBackup()和onRestore的細節。包括讀取各類型文件並調用對應的Helper實現寫入數據到備份文件中以及考慮舊的備份數據的遷移等處理。需要考慮很多細節,代碼量很大

  • 簡單辦法:擴展自系統封裝好的BackupAgentHelper類並告知各類型文件對應的KEY和Helper實現即可,高效而簡單,但沒有提供大容量文件比如DB的備份實現

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

  • SP文件的話SDK提供了特定的SharedPreferencesBackupHelper實現

  • File文件對應的Helper實現為FileBackupHelper,只限於file目錄的數據

  • 其他類型文件比如Data和DB是沒有預設Helper實現的,需要自行實現BackupHelper



// MyBackupAgent.kt  
class MyBackupAgent: BackupAgentHelper() {  
    override fun onCreate() {  
        ...  
        // Init helper for data, file, db and sp files.  
        // Data和DB文件使用FileBackupHelper是無法備份的,此處單純為了驗證下  
        FileBackupHelper(this, Constants.DATA\_NAME).also { addHelper(Constants.BACKUP\_KEY\_DATA, it) }  
        FileBackupHelper(this, Constants.DB\_NAME).also { addHelper(Constants.BACKUP\_KEY\_DB, it) }  
        // File和SP各自使用對應的Helper是可以備份的  
        FileBackupHelper(this, Constants.FILE\_NAME).also { addHelper(Constants.BACKUP\_KEY\_FILE, it) }  
        SharedPreferencesBackupHelper(this, Constants.SP\_NAME).also { addHelper(Constants.BACKUP\_KEY\_SP, it) }  
    }  
    ...  
}  



先用bmgr工具執行Backup,然後清除Demo的數據再執行Restore。從日誌可以看出來鍵值對備份和恢覆成功進行了。



// 開啟bmgr和設置本地傳輸服務  
\>adb shell bmgr enabled  
\>adb shell bmgr transport com.android.localtransport/.LocalTransport  
  
// Backup  
\>adb shell bmgr backupnow com.ellison.backupdemo  
Running incremental backup for 1 requested packages.  
Package @pm@ with result: Success  
Package com.ellison.backupdemo with result: Success  
Backup finished with result: Success  
  
// 清空數據  
\>adb shell pm clear com.ellison.backupdemo  
  
// 查看Backup Token  
\>adb shell dumpsys backup  
...  
Ancestral: 0  
Current:   1  
  
// Restore  
\>adb shell bmgr restore 01 com.ellison.backupdemo  
Scheduling restore: Local disk image  
restoreStarting: 1 packages  
onUpdate: 0 = com.ellison.backupdemo  
restoreFinished: 0  
done  



Demo的截圖顯示File和SP備份和恢覆成功了。但存放在Data目錄的海報和DB目錄都失敗了。這也驗證了上述的結論。

因為出於備份文件空間的考慮,官方並不建議針對DB文件等大容量文件做鍵值對備份。理論上可以擴展FileBackupHelper對Data和DB文件做出支持。但Google將關鍵的備份實現(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功能時需要著重考慮的問題。

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

  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定製任務的時候認真思考下需求再對症下藥。為使得這個流程更加直觀,做了個流程圖分享給大家。

Backup相關屬性

結語

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

  1. 廠商針對Backup功能的Transport擴展可以是Google雲盤也可以是國內伺服器,App開發者需要關註自己的備份需求和安全策略

  2. 思考App是否支持備份,明確開關allowBackup屬性

  3. 更為推薦空間更大、定製靈活的自動備份模式

  4. 儘快適配Android 12封堵數據泄露的風險

  5. 隱私級別很高的數據可以補充設備加密的備份條件在備份階段攔截

  6. 覆寫BackupAgent可以加入恢復的限制,靈活控制流程,在恢復階段二次攔截

Demo地址:

https://github.com/ellisonchan/BackupRestoreApp

  • 提供了鍵值對備份模式的實現

  • 針對自動備份模式預設了備份規則,並定製了限製備份源的恢復流程

尾言

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是系統看門狗WDOG1在i.MXRT1xxx系統啟動中的應用及影響。 軟體看門狗模塊(WDOG)在 MCU 應用里可以說是非常基礎的功能模塊。對於一個產品級的應用程式,如果它沒有使能主控內部的看門狗模塊,一般都不能算是一個合格的軟體設計。 ...
  • 系列文章: Linux Shell 常用命令 - 02篇 0. 線上使用 Linux Shell 參考 https://www.sohu.com/a/343421845_298038 JS/UIX - Terminal 地址:https://www.masswerk.at/jsuix/index.h ...
  • 一、安裝virtualBox 進入 VirtualBox 的主頁,即可進入下載頁面. VirtualBox 是一個跨平臺的虛擬化工具,支持多個操作系統,根據自己的情況選擇對應的版本下載即可。 在安裝完主程式後,直接雙擊擴展包文件即可安裝擴展包。 二、安裝Vagrant 在 Vagant 網站下載最新 ...
  • VM_Ware虛擬機+CentOS 7 系統安裝教程 0.安裝環境 Windows 10 + vmware 15 + centos 7.9 1.準備工作 (1)下載CentOS鏡像 可以訪問阿裡雲的CentOS鏡像網站進行下載 step1:訪問網站 step2:找到對應版本 step3:進入文件夾找 ...
  • testPing.java public class testPing { public static void main(String[] args) { Jedis jedis = new Jedis("Redis節點所在的機器的IP",6379); System.out.println(jed ...
  • 導讀: 本次分享的大綱—— Perception Introduction Sensor Setup & Sensor Fusion Perception Onboard System Perception Technical Challenges -- 01 Perception Introduc ...
  • 1、約束 1.1、唯一性約束(unique) 唯一性約束修飾的欄位具有唯一性,不能重覆。但可以為NULL。 案例:給某一列添加unique drop table if exists t_user; create table t_user( id int, username varchar(255) ...
  • 4 月 23 日,首期 OpenHarmony 開源與開發者成長計劃分享日正式啟動! ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...