注意:對於大多數背景處理用途,我們建議使用 WorkManager 做為建議解決方案。請參閱背景處理指南,瞭解最適合您的解決方案。
在先前的課程中,您已瞭解如何建立同步轉換介面元件來封裝資料移轉程式碼,以及如何新增其他元件,以便將同步轉接器插入系統。現在您已完成安裝內含同步轉接器的應用程式所需的一切資源,但您看到的程式碼實際上並未執行同步轉接器。
建議您嘗試按照時間表或某些事件的間接結果執行同步轉換介面。舉例來說,您可以讓同步轉換介面按照指定時段或一天中的特定時間執行。如果裝置上儲存的資料有所變更,建議您也執行同步轉接器。您應避免以使用者動作的直接結果執行同步轉換介面,因為這麼做無法充分發揮同步轉接器架構的排程功能。舉例來說,請避免在使用者介面中提供重新整理按鈕。
以下提供執行同步轉換介面的選項:
- 伺服器資料變更時
- 針對來自伺服器的訊息執行同步轉換器,表示伺服器資料已變更。這個選項可讓您從伺服器重新整理資料到裝置,而不會因為輪詢伺服器而導致效能降低或浪費電池續航力。
- 裝置資料變更時
- 當裝置上的資料有所變更時,執行同步轉換介面。這個選項可讓您將修改過的資料從裝置傳送到伺服器,這在您需要確保伺服器始終有最新的裝置資料時特別實用。如果實際上是在內容供應器中儲存資料,則這個方法容易實作。如果您使用虛設常式內容供應器,可能會更難以偵測資料變更。
- 定期
- 請在所選時間間隔後執行同步轉換介面,或是在每天的某些時間執行同步轉換介面。
- 隨選
- 執行同步轉換介面以回應使用者動作。不過,如要提供最佳使用者體驗,請主要採用其中一種自動化選項。使用自動化選項可節省電池和網路資源。
本課程其餘部分會詳細說明各個選項。
在伺服器資料變更時執行同步轉換介面
如果應用程式從伺服器轉移資料,且伺服器資料經常變更,您可以使用同步轉換介面執行下載作業以回應資料變更。如要執行同步轉換介面,請讓伺服器傳送特殊訊息至應用程式中的 BroadcastReceiver
。回覆此訊息時,請呼叫 ContentResolver.requestSync()
以向同步轉接器架構發出信號,以便執行同步轉接器。
Google 雲端通訊 (GCM) 提供讓此訊息系統正常運作所需的伺服器和裝置元件。使用 GCM 觸發移轉作業比輪詢伺服器更可靠有效。雖然輪詢需要一律啟用的 Service
,但 GCM 使用在訊息送達時啟動的 BroadcastReceiver
。雖然系統定期輪詢會耗用電池電力,即使沒有可用的更新,GCM 卻只會在需要時傳送訊息。
注意:如果您使用 GCM 透過廣播功能向所有已安裝應用程式的裝置觸發同步轉接器,請記得,訊息的接收時間大致相同。這可能會導致多個同步轉換介面執行個體同時執行,進而造成伺服器和網路超載。為了避免所有裝置在廣播時發生這種情況,建議您考量每部裝置專屬的時段,將同步轉接器的啟動時間延後。
下列程式碼片段說明如何執行 requestSync()
以回應收到的 GCM 訊息:
Kotlin
... // Constants // Content provider authority const val AUTHORITY = "com.example.android.datasync.provider" // Account type const val ACCOUNT_TYPE = "com.example.android.datasync" // Account const val ACCOUNT = "default_account" // Incoming Intent key for extended data const val KEY_SYNC_REQUEST = "com.example.android.datasync.KEY_SYNC_REQUEST" ... class GcmBroadcastReceiver : BroadcastReceiver() { ... override fun onReceive(context: Context, intent: Intent) { // Get a GCM object instance val gcm: GoogleCloudMessaging = GoogleCloudMessaging.getInstance(context) // Get the type of GCM message val messageType: String? = gcm.getMessageType(intent) /* * Test the message type and examine the message contents. * Since GCM is a general-purpose messaging system, you * may receive normal messages that don't require a sync * adapter run. * The following code tests for a a boolean flag indicating * that the message is requesting a transfer from the device. */ if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE == messageType && intent.getBooleanExtra(KEY_SYNC_REQUEST, false)) { /* * Signal the framework to run your sync adapter. Assume that * app initialization has already created the account. */ ContentResolver.requestSync(mAccount, AUTHORITY, null) ... } ... } ... }
Java
public class GcmBroadcastReceiver extends BroadcastReceiver { ... // Constants // Content provider authority public static final String AUTHORITY = "com.example.android.datasync.provider"; // Account type public static final String ACCOUNT_TYPE = "com.example.android.datasync"; // Account public static final String ACCOUNT = "default_account"; // Incoming Intent key for extended data public static final String KEY_SYNC_REQUEST = "com.example.android.datasync.KEY_SYNC_REQUEST"; ... @Override public void onReceive(Context context, Intent intent) { // Get a GCM object instance GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context); // Get the type of GCM message String messageType = gcm.getMessageType(intent); /* * Test the message type and examine the message contents. * Since GCM is a general-purpose messaging system, you * may receive normal messages that don't require a sync * adapter run. * The following code tests for a a boolean flag indicating * that the message is requesting a transfer from the device. */ if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType) && intent.getBooleanExtra(KEY_SYNC_REQUEST)) { /* * Signal the framework to run your sync adapter. Assume that * app initialization has already created the account. */ ContentResolver.requestSync(mAccount, AUTHORITY, null); ... } ... } ... }
在內容供應器資料變更時執行同步轉換介面
如果您的應用程式透過內容供應器收集資料,而您想要在更新供應器時更新伺服器,可以設定讓應用程式自動執行同步轉換器。方法是為內容供應器註冊觀察器。當內容供應器中的資料有所變更時,內容供應器架構會呼叫觀察器。在觀察器中呼叫 requestSync()
,告知架構執行同步轉接器。
注意:如果您使用虛設常式內容供應器,內容供應器中沒有任何資料,系統也絕對不會呼叫 onChange()
。在這種情況下,您必須自行提供用於偵測裝置資料變更的機制。在資料變更時,此機制也會負責呼叫 requestSync()
。
如要為內容供應器建立觀察器,請擴充類別 ContentObserver
,並實作這兩種形式的 onChange()
方法。在 onChange()
中呼叫 requestSync()
,啟動同步轉換介面。
如要註冊觀察器,請在呼叫 registerContentObserver()
時將其做為引數傳遞。在此呼叫中,您也必須針對要查看的資料傳入內容 URI。內容供應器架構會將這個手錶 URI 和做為引數傳遞的內容 URI 與修改供應器的 ContentResolver
方法進行比較,例如 ContentResolver.insert()
。如果有相符項目,系統會呼叫您的 ContentObserver.onChange()
實作項目。
下列程式碼片段說明如何定義 ContentObserver
,以便在資料表變更時呼叫 requestSync()
:
Kotlin
// Constants // Content provider scheme const val SCHEME = "content://" // Content provider authority const val AUTHORITY = "com.example.android.datasync.provider" // Path for the content provider table const val TABLE_PATH = "data_table" ... class MainActivity : FragmentActivity() { ... // A content URI for the content provider's data table private lateinit var uri: Uri // A content resolver for accessing the provider private lateinit var mResolver: ContentResolver ... inner class TableObserver(...) : ContentObserver(...) { /* * Define a method that's called when data in the * observed content provider changes. * This method signature is provided for compatibility with * older platforms. */ override fun onChange(selfChange: Boolean) { /* * Invoke the method signature available as of * Android platform version 4.1, with a null URI. */ onChange(selfChange, null) } /* * Define a method that's called when data in the * observed content provider changes. */ override fun onChange(selfChange: Boolean, changeUri: Uri?) { /* * Ask the framework to run your sync adapter. * To maintain backward compatibility, assume that * changeUri is null. */ ContentResolver.requestSync(account, AUTHORITY, null) } ... } ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... // Get the content resolver object for your app mResolver = contentResolver // Construct a URI that points to the content provider data table uri = Uri.Builder() .scheme(SCHEME) .authority(AUTHORITY) .path(TABLE_PATH) .build() /* * Create a content observer object. * Its code does not mutate the provider, so set * selfChange to "false" */ val observer = TableObserver(false) /* * Register the observer for the data table. The table's path * and any of its subpaths trigger the observer. */ mResolver.registerContentObserver(uri, true, observer) ... } ... }
Java
public class MainActivity extends FragmentActivity { ... // Constants // Content provider scheme public static final String SCHEME = "content://"; // Content provider authority public static final String AUTHORITY = "com.example.android.datasync.provider"; // Path for the content provider table public static final String TABLE_PATH = "data_table"; // Account public static final String ACCOUNT = "default_account"; // Global variables // A content URI for the content provider's data table Uri uri; // A content resolver for accessing the provider ContentResolver mResolver; ... public class TableObserver extends ContentObserver { /* * Define a method that's called when data in the * observed content provider changes. * This method signature is provided for compatibility with * older platforms. */ @Override public void onChange(boolean selfChange) { /* * Invoke the method signature available as of * Android platform version 4.1, with a null URI. */ onChange(selfChange, null); } /* * Define a method that's called when data in the * observed content provider changes. */ @Override public void onChange(boolean selfChange, Uri changeUri) { /* * Ask the framework to run your sync adapter. * To maintain backward compatibility, assume that * changeUri is null. */ ContentResolver.requestSync(mAccount, AUTHORITY, null); } ... } ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... // Get the content resolver object for your app mResolver = getContentResolver(); // Construct a URI that points to the content provider data table uri = new Uri.Builder() .scheme(SCHEME) .authority(AUTHORITY) .path(TABLE_PATH) .build(); /* * Create a content observer object. * Its code does not mutate the provider, so set * selfChange to "false" */ TableObserver observer = new TableObserver(false); /* * Register the observer for the data table. The table's path * and any of its subpaths trigger the observer. */ mResolver.registerContentObserver(uri, true, observer); ... } ... }
定期執行同步轉換介面
如要定期執行同步轉換介面,請設定每次執行之間等待的等待時間,或於一天中的特定時間執行同步轉換介面,也可以同時執行兩者。定期執行同步轉換器可讓您大約符合伺服器的更新時間間隔。
同樣地,您可以在伺服器處於閒置狀態時,將同步轉換介面安排在夜間執行,藉此從裝置上傳資料。大多數使用者在晚上會維持開機並接上電源,因此這通常是空閒時間。而且裝置與同步轉接器不會同時執行其他工作。不過,如果採取這種做法,您必須確保每部裝置觸發資料移轉的時間會稍有不同。如果所有裝置同時執行同步轉接器,可能會導致伺服器和行動服務供應商的資料網路超載。
一般來說,如果使用者不需要立即更新,但希望定期更新,定期執行就很合理。此外,您也可定期執行定期執行作業,藉此平衡最新資料的可用性,同時重視不會過度使用裝置資源的小型同步轉換介面執行效率。
如要定期執行同步轉換介面,請呼叫 addPeriodicSync()
。這會安排同步處理轉換介面,在經過一段時間後執行。由於同步轉換介面架構必須考量其他同步轉換介面執行作業,並嘗試盡可能延長電池效率,因此經過時間可能會與幾秒鐘不同。此外,如果網路無法使用,架構也不會執行同步轉接器。
請注意,addPeriodicSync()
不會在特定時間執行同步轉換介面。如要每天大致相同的時間執行同步轉換介面,請使用週期性鬧鐘做為觸發條件。如要進一步瞭解週期性鬧鐘,請參閱 AlarmManager
的參考說明文件。如果您使用 setInexactRepeating()
方法設定包含變動的時段觸發條件,您仍應隨機選擇開始時間,確保同步轉接器會在不同的裝置上執行。
addPeriodicSync()
方法不會停用 setSyncAutomatically()
,因此在短時間內可能會有多次同步處理執行作業。此外,呼叫 addPeriodicSync()
時只能使用幾個同步轉接介面控制旗標,其中 addPeriodicSync()
的參考文件將說明系統不允許的標記。
下列程式碼片段說明如何排定定期的同步處理轉換器執行時間:
Kotlin
// Content provider authority const val AUTHORITY = "com.example.android.datasync.provider" // Account const val ACCOUNT = "default_account" // Sync interval constants const val SECONDS_PER_MINUTE = 60L const val SYNC_INTERVAL_IN_MINUTES = 60L const val SYNC_INTERVAL = SYNC_INTERVAL_IN_MINUTES * SECONDS_PER_MINUTE ... class MainActivity : FragmentActivity() { ... // A content resolver for accessing the provider private lateinit var mResolver: ContentResolver override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... // Get the content resolver for your app mResolver = contentResolver /* * Turn on periodic syncing */ ContentResolver.addPeriodicSync( mAccount, AUTHORITY, Bundle.EMPTY, SYNC_INTERVAL) ... } ... }
Java
public class MainActivity extends FragmentActivity { ... // Constants // Content provider authority public static final String AUTHORITY = "com.example.android.datasync.provider"; // Account public static final String ACCOUNT = "default_account"; // Sync interval constants public static final long SECONDS_PER_MINUTE = 60L; public static final long SYNC_INTERVAL_IN_MINUTES = 60L; public static final long SYNC_INTERVAL = SYNC_INTERVAL_IN_MINUTES * SECONDS_PER_MINUTE; // Global variables // A content resolver for accessing the provider ContentResolver mResolver; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... // Get the content resolver for your app mResolver = getContentResolver(); /* * Turn on periodic syncing */ ContentResolver.addPeriodicSync( mAccount, AUTHORITY, Bundle.EMPTY, SYNC_INTERVAL); ... } ... }
視需求執行同步轉換介面
執行同步轉換器來回應使用者要求,是執行同步轉接程式最不適當的策略。此架構經過特別設計,可依時間表執行同步轉換器時節省電池電力。為因應資料變更而執行同步處理的選項,由於電力會用來提供新資料,因此有效使用電池電力。
相較之下,如果允許使用者隨選執行同步處理,則同步處理作業會自行執行,這會降低網路和電力資源的使用效率。此外,即使沒有證據顯示資料有所變更,提供隨選同步仍會導致使用者要求同步處理;而執行不會重新整理資料的同步處理作業,會導致電池電力耗盡。一般來說,應用程式應使用其他信號觸發同步處理作業,或是安排定期執行同步處理作業,不必使用者輸入內容。
不過,如果您仍想視需求執行同步轉換介面,請設定同步處理轉接器標記來執行手動同步轉換介面,然後呼叫 ContentResolver.requestSync()
。
使用下列旗標執行隨選移轉作業:
-
SYNC_EXTRAS_MANUAL
-
強制執行手動同步處理。同步轉換介面架構會忽略現有設定,例如
setSyncAutomatically()
設定的旗標。 -
SYNC_EXTRAS_EXPEDITED
- 強制同步處理立即啟動。如果不設定這個值,系統可能會等待數秒後才執行同步處理要求,因為會嘗試在短時間內安排多項要求,藉此最佳化電池用量。
下列程式碼片段說明如何呼叫 requestSync()
以回應按鈕點擊:
Kotlin
// Constants // Content provider authority val AUTHORITY = "com.example.android.datasync.provider" // Account type val ACCOUNT_TYPE = "com.example.android.datasync" // Account val ACCOUNT = "default_account" ... class MainActivity : FragmentActivity() { ... // Instance fields private lateinit var mAccount: Account ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... /* * Create the placeholder account. The code for CreateSyncAccount * is listed in the lesson Creating a Sync Adapter */ mAccount = createSyncAccount() ... } /** * Respond to a button click by calling requestSync(). This is an * asynchronous operation. * * This method is attached to the refresh button in the layout * XML file * * @param v The View associated with the method call, * in this case a Button */ fun onRefreshButtonClick(v: View) { // Pass the settings flags by inserting them in a bundle val settingsBundle = Bundle().apply { putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) } /* * Request the sync for the default account, authority, and * manual sync settings */ ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle) }
Java
public class MainActivity extends FragmentActivity { ... // Constants // Content provider authority public static final String AUTHORITY = "com.example.android.datasync.provider"; // Account type public static final String ACCOUNT_TYPE = "com.example.android.datasync"; // Account public static final String ACCOUNT = "default_account"; // Instance fields Account mAccount; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... /* * Create the placeholder account. The code for CreateSyncAccount * is listed in the lesson Creating a Sync Adapter */ mAccount = CreateSyncAccount(this); ... } /** * Respond to a button click by calling requestSync(). This is an * asynchronous operation. * * This method is attached to the refresh button in the layout * XML file * * @param v The View associated with the method call, * in this case a Button */ public void onRefreshButtonClick(View v) { // Pass the settings flags by inserting them in a bundle Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean( ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean( ContentResolver.SYNC_EXTRAS_EXPEDITED, true); /* * Request the sync for the default account, authority, and * manual sync settings */ ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle); }