Android 備份服務提供雲端儲存空間備份與還原功能,可儲存 Android 應用程式中的鍵/值資料。在鍵/值備份作業期間,應用程式的備份資料會傳送至裝置的備份傳輸。如果裝置使用預設的 Google 備份傳輸,資料會傳送至 Android Backup Service 進行封存。
每位應用程式使用者的資料上限為 5 MB。儲存備份資料不必付費。
如需 Android 備份選項的總覽資訊,以及您應備份及還原的資料,請參閱「資料備份總覽」。
實作鍵/值備份
如要備份應用程式資料,您必須實作備份代理程式。備份與還原期間,備份管理員會呼叫您的備份代理程式。
如要實作備份代理程式,您必須:
使用
android:backupAgent屬性在資訊清單檔案中宣告備份代理程式。採取下列任一行動來定義備份代理程式:
-
BackupAgent類別提供了一個中央介面,方便您的應用程式與備份管理員通訊。如果直接擴充這個類別,則必須覆寫onBackup()和onRestore()來處理備份並為您的資料進行還原操作。 -
BackupAgentHelper類別為BackupAgent類別提供了便利的包裝函式,可減少您需要編寫的程式碼。您必須在BackupAgentHelper中使用一或多個輔助物件,藉此自動備份及復原特定類型的資料,這樣就不必實作onBackup()和onRestore()。除非您需要完整控制應用程式備份,否則建議您使用BackupAgentHelper處理應用程式的備份。Android 目前提供備份輔助程式,以便備份和還原
SharedPreferences和內部儲存空間中的完整檔案。
-
在資訊清單中宣告備份代理程式
決定備份代理程式的類別名稱後,請使用 <application> 標記中的 android:backupAgent 屬性在資訊清單中宣告。
例如:
<manifest ... >
...
<application android:label="MyApplication"
android:backupAgent="MyBackupAgent">
<meta-data android:name="com.google.android.backup.api_key"
android:value="unused" />
<activity ... >
...
</activity>
</application>
</manifest>
如要支援較舊的裝置,建議您在 Android 資訊清單檔案中加入 API 金鑰 <meta-data>。Android 備份服務不再需要服務金鑰,但部分較舊的裝置在備份時可能仍會檢查金鑰。將 android:name 設為 com.google.android.backup.api_key,並將 android:value 設為 unused。
無論當前應用程式版本與生成備份資料的版本有何差別,android:restoreAnyVersion 屬性使用布林值,指出您是否要還原應用程式資料。預設值為 false。詳情請參閱「查看還原資料版本」一節。
擴充 BackupAgentHelper
如要備份來自 SharedPreferences 或內部儲存空間的完整檔案,請使用 BackupAgentHelper 建立備份代理程式。使用 BackupAgentHelper 建立備份代理程式所需的程式碼遠少於擴充 BackupAgent,因為您不需要實作 onBackup() 和 onRestore()。
實作 BackupAgentHelper 時必須使用一個或多個備份輔助程式。備份輔助程式是一種特殊元件,BackupAgentHelper 可叫出為特定類型的資料執行備份和還原操作。Android 架構目前提供兩種不同的輔助程式:
SharedPreferencesBackupHelper來備份SharedPreferences檔案。FileBackupHelper備份內部儲存空間中的檔案。
您可以在 BackupAgentHelper 中加入多個輔助程式,但每種資料類型只需要一個輔助程式。也就是說,如果您有多個 SharedPreferences 檔案,您只需要一個 SharedPreferencesBackupHelper。
如要對 BackupAgentHelper 新增每個輔助程式,請務必在 onCreate() 方法中執行下列步驟:
- 對所選輔助類別執行個體化。在類別建構函式中,您必須指定要備份的檔案。
- 呼叫
addHelper()即可將輔助程式新增至BackupAgentHelper。
下列各節說明如何透過每個可用的輔助程式建立備份代理程式。
備份 SharedPreferences
將 SharedPreferencesBackupHelper 執行個體化時,您必須加入一個或多個 SharedPreferences 檔案的名稱。
舉例來說,如要備份名為 user_preferences 的 SharedPreferences 檔案,使用 BackupAgentHelper 的完整備份代理程式如下所示:
Kotlin
// The name of the SharedPreferences file
const val PREFS = "user_preferences"
// A key to uniquely identify the set of backup data
const val PREFS_BACKUP_KEY = "prefs"
class MyPrefsBackupAgent : BackupAgentHelper() {
override fun onCreate() {
// Allocate a helper and add it to the backup agent
SharedPreferencesBackupHelper(this, PREFS).also {
addHelper(PREFS_BACKUP_KEY, it)
}
}
}
Java
public class MyPrefsBackupAgent extends BackupAgentHelper {
// The name of the SharedPreferences file
static final String PREFS = "user_preferences";
// A key to uniquely identify the set of backup data
static final String PREFS_BACKUP_KEY = "prefs";
// Allocate a helper and add it to the backup agent
@Override
public void onCreate() {
SharedPreferencesBackupHelper helper =
new SharedPreferencesBackupHelper(this, PREFS);
addHelper(PREFS_BACKUP_KEY, helper);
}
}
SharedPreferencesBackupHelper 包含備份及還原 SharedPreferences 檔案所需的所有程式碼。
當備份管理員呼叫 onBackup() 和 onRestore() 時,BackupAgentHelper 會呼叫備用輔助程式來備份及還原指定的檔案。
備份其他檔案
為 FileBackupHelper 執行個體化時,您必須加入 getFilesDir() 指定的一個或多個儲存在應用程式內部儲存空間中的檔案名稱,與 openFileOutput() 寫入檔案的位置相同。
舉例來說,如要備份兩個名稱為 scores 和 stats 的檔案,使用 BackupAgentHelper 的備份代理程式如下所示:
Kotlin
// The name of the file
const val TOP_SCORES = "scores"
const val PLAYER_STATS = "stats"
// A key to uniquely identify the set of backup data
const val FILES_BACKUP_KEY = "myfiles"
class MyFileBackupAgent : BackupAgentHelper() {
override fun onCreate() {
// Allocate a helper and add it to the backup agent
FileBackupHelper(this, TOP_SCORES, PLAYER_STATS).also {
addHelper(FILES_BACKUP_KEY, it)
}
}
}
Java
public class MyFileBackupAgent extends BackupAgentHelper {
// The name of the file
static final String TOP_SCORES = "scores";
static final String PLAYER_STATS = "stats";
// A key to uniquely identify the set of backup data
static final String FILES_BACKUP_KEY = "myfiles";
// Allocate a helper and add it to the backup agent
@Override
public void onCreate() {
FileBackupHelper helper = new FileBackupHelper(this,
TOP_SCORES, PLAYER_STATS);
addHelper(FILES_BACKUP_KEY, helper);
}
}
FileBackupHelper 包含備份及還原儲存在應用程式內部儲存空間中的所有程式碼。
不過,讀取及寫入內部儲存空間的檔案並非執行緒安全。為了確保備份代理程式不會同時讀取或寫入檔案,每次執行讀取或寫入作業時都必須使用同步陳述式。例如,在讀取及寫入檔案的任何活動中,您需要一個物件做為同步陳述式的內建鎖定:
Kotlin
// Object for intrinsic lock
companion object {
val sDataLock = Any()
}
Java
// Object for intrinsic lock
static final Object sDataLock = new Object();
每當您讀取或寫入檔案時,即可建立具有此鎖定的同步陳述式。例如,以下是將遊戲的最新分數寫入檔案的同步處理陳述式:
Kotlin
try {
synchronized(MyActivity.sDataLock) {
val dataFile = File(filesDir, TOP_SCORES)
RandomAccessFile(dataFile, "rw").apply {
writeInt(score)
}
}
} catch (e: IOException) {
Log.e(TAG, "Unable to write to file")
}
Java
try {
synchronized (MyActivity.sDataLock) {
File dataFile = new File(getFilesDir(), TOP_SCORES);
RandomAccessFile raFile = new RandomAccessFile(dataFile, "rw");
raFile.writeInt(score);
}
} catch (IOException e) {
Log.e(TAG, "Unable to write to file");
}
您應使用相同的鎖定同步讀取陳述式。
然後,在 BackupAgentHelper 中,您必須覆寫 onBackup() 和 onRestore(),才能以相同的內建鎖定方式同步處理備份和還原作業。舉例來說,上述的 MyFileBackupAgent 範例需要下列方法:
Kotlin
@Throws(IOException::class)
override fun onBackup(
oldState: ParcelFileDescriptor,
data: BackupDataOutput,
newState: ParcelFileDescriptor
) {
// Hold the lock while the FileBackupHelper performs back up
synchronized(MyActivity.sDataLock) {
super.onBackup(oldState, data, newState)
}
}
@Throws(IOException::class)
override fun onRestore(
data: BackupDataInput,
appVersionCode: Int,
newState: ParcelFileDescriptor
) {
// Hold the lock while the FileBackupHelper restores the file
synchronized(MyActivity.sDataLock) {
super.onRestore(data, appVersionCode, newState)
}
}
Java
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
// Hold the lock while the FileBackupHelper performs back up
synchronized (MyActivity.sDataLock) {
super.onBackup(oldState, data, newState);
}
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
// Hold the lock while the FileBackupHelper restores the file
synchronized (MyActivity.sDataLock) {
super.onRestore(data, appVersionCode, newState);
}
}
擴充 BackupAgent
大多數應用程式均不需要直接擴充 BackupAgent 類別,而是會擴充 BackupAgentHelper,利用自動備份和還原檔案的內建輔助類別。不過,您可以直接展開 BackupAgent 進行以下操作:
- 版本化資料格式。舉例來說,如果您預期需要修改寫入應用程式資料的格式,可以建立備份代理程式,在還原作業期間交叉檢查應用程式版本,並在裝置上的版本與備份資料的版本不同時,執行任何必要的相容性工作。詳情請參閱「檢查還原資料版本」。
- 指定要備份的資料部分。您可以指定不要備份整個檔案,而是指定要備份的資料部分,以及將各部分還原到裝置的方式。這樣做還能協助您管理不同版本,因為您讀取和寫入的資料是不重複實體,而非完整的檔案。
- 備份資料庫中的資料。如要在使用者重新安裝您的應用程式時還原 SQL SQL 資料庫,您必須建立自訂
BackupAgent,以便在備份作業期間讀取相關資料,然後建立還原作業,並在還原作業中插入資料。
如果您不需要執行上述任何任務,並想備份 SharedPreferences 或內部儲存空間的完整檔案,請參閱「擴充 BackupAgentHelper」一節。
必要方法
建立 BackupAgent 時,您必須實作下列回呼方法:
onBackup()- 備份管理員會在您要求備份之後呼叫此方法。在這個方法中,您必須從裝置讀取應用程式資料,並將要備份的資料傳遞到備份管理員,如「執行備份」一節所述。
onRestore()備份管理員會在還原作業中呼叫這個方法。您的應用程式可以利用這個方法提供的備份資料還原先前的狀態,如「執行還原作業」一節所述。
使用者重新安裝應用程式時,系統會呼叫這個方法來還原備份資料,但應用程式也可以要求還原資料。
執行備份
備份要求不會立即呼叫 onBackup() 方法。相反地,備份管理員會等到適當的時間,然後針對自上次備份執行以來,曾要求備份的所有應用程式執行備份。此時,您必須將應用程式資料提供給備份管理員,才能將資料儲存至雲端儲存空間。
只有備份管理員可呼叫備份代理程式的 onBackup() 方法。每次應用程式資料變更且想要執行備份時,您必須呼叫 dataChanged() 來要求備份作業。詳情請參閱申請備份一文。
提示:開發應用程式時,您可以透過 bmgr 工具從備份管理員處啟動即時備份作業。
當備份管理員呼叫您的 onBackup() 方法時,會傳遞三個參數:
oldState- 開放式唯讀
ParcelFileDescriptor指向應用程式最近一次的備份狀態。這並非雲端儲存空間中的備份資料,而是本機最近一次呼叫onBackup()時備份資料的本機表示法,並依newState或onRestore()定義。下節會介紹onRestore()。由於onBackup()不允許您讀取雲端儲存空間中的現有備份資料,因此您可以使用這個本機表示法來判斷資料自上次備份後是否有所變更。 dataBackupDataOutput物件,可用來將備份資料傳送至備份管理員。newState- 開啟/讀取寫入的
ParcelFileDescriptor指向檔案,您必須在其中寫入您傳送至data的資料表示法。表示法可以是檔案上次修改的時間戳記。下次備份管理員呼叫您的onBackup()方法時,此物件將作為oldState傳回。如果您未將備份資料寫入newState,則oldState會在下次透過備份管理員呼叫onBackup()時指向空白檔案。
使用這些參數可以實作 onBackup() 方法,如下所示:
比較
oldState與您目前的資料,藉此確認自上次備份後的資料是否已變更。在oldState中讀取資料的方式,取決於您將資料寫入newState的方式 (請參閱步驟 3)。如要記錄檔案狀態,最簡單的方法是使用上次修改的時間戳記。以下範例說明如何讀取及比較oldState的時間戳記:Kotlin
val instream = FileInputStream(oldState.fileDescriptor) val dataInputStream = DataInputStream(instream) try { // Get the last modified timestamp from the state file and data file val stateModified = dataInputStream.readLong() val fileModified: Long = dataFile.lastModified() if (stateModified != fileModified) { // The file has been modified, so do a backup // Or the time on the device changed, so be safe and do a backup } else { // Don't back up because the file hasn't changed return } } catch (e: IOException) { // Unable to read state file... be safe and do a backup }Java
// Get the oldState input stream FileInputStream instream = new FileInputStream(oldState.getFileDescriptor()); DataInputStream in = new DataInputStream(instream); try { // Get the last modified timestamp from the state file and data file long stateModified = in.readLong(); long fileModified = dataFile.lastModified(); if (stateModified != fileModified) { // The file has been modified, so do a backup // Or the time on the device changed, so be safe and do a backup } else { // Don't back up because the file hasn't changed return; } } catch (IOException e) { // Unable to read state file... be safe and do a backup }如果沒有任何變更且不需要備份,請跳到步驟 3。
如果資料已變更,與
oldState相比,請將目前資料寫入data,以將資料備份到雲端儲存空間。您必須將
BackupDataOutput的資料區塊寫入實體。實體是一種以不重複的金鑰字串識別的扁平二進位資料記錄。因此,您備份的資料集實際上是一組鍵/值組合。如要將實體新增至備份資料集,您必須:
呼叫
writeEntityHeader(),針對您要寫入的資料和資料大小傳遞不重複字串鍵。呼叫
writeEntityData()並傳遞包含您的資料的位元組緩衝區,以及要寫入的緩衝區位元組數 (應與傳送至writeEntityHeader()的大小相符)。
例如,下列程式碼會將部分資料扁平化為位元組串流,並寫入單一實體:
Kotlin
val buffer: ByteArray = ByteArrayOutputStream().run { DataOutputStream(this).apply { writeInt(playerName) writeInt(playerScore) } toByteArray() } val len: Int = buffer.size data.apply { writeEntityHeader(TOPSCORE_BACKUP_KEY, len) writeEntityData(buffer, len) }Java
// Create buffer stream and data output stream for our data ByteArrayOutputStream bufStream = new ByteArrayOutputStream(); DataOutputStream outWriter = new DataOutputStream(bufStream); // Write structured data outWriter.writeUTF(playerName); outWriter.writeInt(playerScore); // Send the data to the Backup Manager using the BackupDataOutput byte[] buffer = bufStream.toByteArray(); int len = buffer.length; data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len); data.writeEntityData(buffer, len);請針對要備份的每項資料執行這項作業。資料轉移至實體的方式完全由您決定。您甚至可能只用一個實體。
不論您是否執行了備份 (步驟 2),請將目前資料寫入
newStateParcelFileDescriptor。備份管理員會將物件保留在本機,做為目前備份資料的表示法。這個指令會在下次呼叫onBackup()時,以oldState的形式傳回,方便您確認是否需要其他備份 (如步驟 1 所述)。如果您不將目前的資料狀態寫入這個檔案,則下次回呼時,oldState會留空。以下範例使用檔案上次修改的時間戳記,將目前資料的表示法儲存至
newState中:Kotlin
val modified = dataFile.lastModified() FileOutputStream(newState.fileDescriptor).also { DataOutputStream(it).apply { writeLong(modified) } }Java
FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor()); DataOutputStream out = new DataOutputStream(outstream); long modified = dataFile.lastModified(); out.writeLong(modified);
執行還原作業
需要還原應用程式資料時,備份管理員會呼叫備份代理程式的 onRestore() 方法。呼叫此方法時,備份管理員會傳送備份資料,讓您將資料還原到裝置上。
只有備份管理員可以呼叫 onRestore(),這會在系統安裝應用程式及尋找現有的備份資料時自動進行此作業。
當備份管理員呼叫您的 onRestore() 方法時,會傳遞三個參數:
dataBackupDataInput物件,可讓您讀取備份資料。appVersionCode- 一個整數代表應用程式的
android:versionCode資訊清單屬性值,就如同備份這些資料時一樣。您可以使用這組憑證來檢查目前的應用程式版本,並判斷資料格式是否相容。如要進一步瞭解如何使用這個方法處理不同版本的還原資料,請參閱「查看還原資料版本」。 newState- 開啟的讀取/寫入
ParcelFileDescriptor,指向您在檔案中必須針對data提供的最終備份狀態。這個物件會在下次呼叫onBackup()時以oldState傳回。提醒您,您也必須在onBackup()回呼中寫入相同的newState物件,且在此也會確保oldState給予onBackup()的物件有效 (即使裝置在還原後首次呼叫onBackup()也一樣)。
實作 onRestore() 時,您應該針對 data 呼叫 readNextHeader(),以疊代處理資料集中的所有實體。針對找到的每個實體執行以下操作:
- 使用
getKey()取得實體金鑰。 將實體鍵與
BackupAgent類別 (應宣告為靜態最終字串) 的已知鍵/值清單進行比對。當金鑰與其中一個已知的金鑰字串相符時,請輸入陳述式來擷取實體資料,然後儲存至裝置:- 使用
getDataSize()取得實體資料大小,並建立該大小的位元組陣列。 - 呼叫
readEntityData()並向其傳遞位元組陣列 (即資料插入的位置),並且指定開始偏移和要讀取的大小。 - 位元組陣列已滿。視需要讀取資料並寫入裝置。
- 使用
讀取資料並將資料寫入裝置後,請像在
onBackup()中一樣,將資料的狀態寫入newState參數。
舉例來說,以下是還原上一節範例備份的資料:
Kotlin
@Throws(IOException::class)
override fun onRestore(data: BackupDataInput, appVersionCode: Int,
newState: ParcelFileDescriptor) {
with(data) {
// There should be only one entity, but the safest
// way to consume it is using a while loop
while (readNextHeader()) {
when(key) {
TOPSCORE_BACKUP_KEY -> {
val dataBuf = ByteArray(dataSize).also {
readEntityData(it, 0, dataSize)
}
ByteArrayInputStream(dataBuf).also {
DataInputStream(it).apply {
// Read the player name and score from the backup data
playerName = readUTF()
playerScore = readInt()
}
// Record the score on the device (to a file or something)
recordScore(playerName, playerScore)
}
}
else -> skipEntityData()
}
}
}
// Finally, write to the state blob (newState) that describes the restored data
FileOutputStream(newState.fileDescriptor).also {
DataOutputStream(it).apply {
writeUTF(playerName)
writeInt(mPlayerScore)
}
}
}
Java
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
// There should be only one entity, but the safest
// way to consume it is using a while loop
while (data.readNextHeader()) {
String key = data.getKey();
int dataSize = data.getDataSize();
// If the key is ours (for saving top score). Note this key was used when
// we wrote the backup entity header
if (TOPSCORE_BACKUP_KEY.equals(key)) {
// Create an input stream for the BackupDataInput
byte[] dataBuf = new byte[dataSize];
data.readEntityData(dataBuf, 0, dataSize);
ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
DataInputStream in = new DataInputStream(baStream);
// Read the player name and score from the backup data
playerName = in.readUTF();
playerScore = in.readInt();
// Record the score on the device (to a file or something)
recordScore(playerName, playerScore);
} else {
// We don't know this entity key. Skip it. (Shouldn't happen.)
data.skipEntityData();
}
}
// Finally, write to the state blob (newState) that describes the restored data
FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
DataOutputStream out = new DataOutputStream(outstream);
out.writeUTF(playerName);
out.writeInt(mPlayerScore);
}
在這個範例中,系統不會使用傳送至 onRestore() 的 appVersionCode 參數。不過,如果您選擇在使用者的應用程式版本實際降級時執行備份 (例如使用者從應用程式的 1.5 版改為 1.0 版),建議您使用該參數。詳情請參閱下一節。
查看還原資料版本
備份管理員將資料儲存到雲端儲存空間時,會自動由資訊清單檔案的 android:versionCode 屬性定義。在備份管理員呼叫備份代理程式還原資料之前,請先查看已安裝應用程式的 android:versionCode,並與備份資料集中記錄的值進行比較。如果還原資料集中記錄的版本新於裝置上的應用程式版本,表示使用者已降級應用程式。在這種情況下,備份管理員會取消應用程式的還原作業,且不會呼叫 onRestore() 方法,因為還原集對舊版本而言沒有意義。
您可以使用 android:restoreAnyVersion 屬性覆寫這個行為:
false(預設):如果還原資料集中記錄的版本比裝置上的應用程式版本新,備份管理工具會取消還原作業。true:備份管理員會忽略android:versionCode,並一律呼叫onRestore()方法,因此您可以手動檢查版本差異,並確保資料相容。
為了協助您在還原作業中處理不同版本,onRestore() 方法會將還原資料集所包含的版本代碼傳送至 appVersionCode 參數。然後,您可以使用 PackageInfo.versionCode 欄位查詢目前的應用程式版本代碼。例如:
Kotlin
val info: PackageInfo? = try {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
val version: Int = info?.versionCode ?: 0
Java
PackageInfo info;
try {
String name = getPackageName();
info = getPackageManager().getPackageInfo(name, 0);
} catch (NameNotFoundException nnfe) {
info = null;
}
int version;
if (info != null) {
version = info.versionCode;
}
然後比較從 PackageInfo 取得的 version 和傳入 onRestore() 的 appVersionCode。
申請備份
你隨時可以呼叫 dataChanged() 來要求備份。這個方法會通知備份管理員您要使用備份代理程式備份資料。備份管理員日後會呼叫備份代理程式的 onBackup() 方法。一般來說,您應在每次資料變更時提出備份 (例如使用者變更您要備份的應用程式偏好設定時)。如果您在備份管理員向代理程式提出備份要求前多次呼叫 dataChanged(),代理程式仍只會收到對 onBackup() 的一次呼叫。
提出還原要求
在應用程式的正常執行期間,您不需要要求還原作業。系統會自動檢查備份資料,並在安裝應用程式時執行還原作業。
遷移至自動備份
您可以將 android:fullBackupOnly 設為 true,在資訊清單檔案的 <application> 元素中將應用程式轉換為完整資料備份。在搭載 Android 5.1 (API 級別 22) 或以下版本的裝置上執行應用程式時,應用程式會忽略資訊清單中的這個值,並繼續執行鍵/值備份。在搭載 Android 6.0 (API 級別 23) 或以上版本的裝置上執行時,應用程式會執行自動備份,而非備份鍵/值。
使用者隱私
Google 肩負您的信任,我們深知自己有責任保護使用者隱私。Google 會透過 Google 伺服器安全地傳輸備份資料,以提供備份與還原功能。Google 會依據自家《隱私權政策》將這項資料視為個人資訊。
此外,使用者則可透過 Android 系統的備份設定停用資料備份功能。使用者停用備份時,Android 備份服務會刪除所有已儲存的備份資料。使用者可以在裝置上重新啟用備份功能,但 Android Backup Service 不會還原任何先前刪除的資料。