執行同步轉換介面

注意:對於大多數背景處理用途,我們建議使用 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);
    }