Android 執(zhí)行 Sync Adpater

2018-08-02 17:44 更新

編寫:jdneo - 原文:http://developer.android.com/training/sync-adapters/running-sync-adapter.html

在本節(jié)課之前,我們已經(jīng)學(xué)習(xí)了如何創(chuàng)建一個封裝了數(shù)據(jù)傳輸代碼的 Sync Adapter 組件,以及如何添加其它的組件,使得我們可以將 Sync Adapter 集成到系統(tǒng)當(dāng)中?,F(xiàn)在我們已經(jīng)擁有了所有部件,來安裝一個包含有 Sync Adapter 的應(yīng)用了,但是這里還沒有任何代碼是負責(zé)去運行 Sync Adapter。

執(zhí)行 Sync Adapter 的時機,一般應(yīng)該基于某個計劃任務(wù)或者一些事件的間接結(jié)果。例如,我們可能希望 Sync Adapter 以一個定期計劃任務(wù)的形式運行(比如每隔一段時間或者在每天的一個固定時間運行)?;蛘咭部赡芟M?dāng)設(shè)備上的數(shù)據(jù)發(fā)生變化后,執(zhí)行 Sync Adapter。我們應(yīng)該避免將運行 Sync Adapter 作為用戶某個行為的直接結(jié)果,因為這樣做的話我們就無法利用 Sync Adapter 框架可以按計劃調(diào)度的特性。例如,我們應(yīng)該在 UI 中避免使用刷新按鈕。

下列情況可以作為運行 Sync Adapter 的時機:

當(dāng)服務(wù)端數(shù)據(jù)變更時:

當(dāng)服務(wù)端發(fā)送消息告知服務(wù)端數(shù)據(jù)發(fā)生變化時,運行 Sync Adapter 以響應(yīng)這一來自服務(wù)端的消息。這一選項允許從服務(wù)器更新數(shù)據(jù)到設(shè)備上,該方法可以避免由于輪詢服務(wù)器所造成的執(zhí)行效率下降,或者電量損耗。

當(dāng)設(shè)備的數(shù)據(jù)變更時:

當(dāng)設(shè)備上的數(shù)據(jù)發(fā)生變化時,運行 Sync Adapter。這一選項允許我們將修改后的數(shù)據(jù)從設(shè)備發(fā)送給服務(wù)器。如果需要保證服務(wù)器端一直擁有設(shè)備上最新的數(shù)據(jù),那么這一選項非常有用。如果我們將數(shù)據(jù)存儲于 Content Provider,那么這一選項的實現(xiàn)將會非常直接。如果使用的是一個 Stub Content Provider,檢測數(shù)據(jù)的變化可能會比較困難。

當(dāng)系統(tǒng)發(fā)送了一個網(wǎng)絡(luò)消息:

當(dāng) Android 系統(tǒng)發(fā)送了一個網(wǎng)絡(luò)消息來保持 TCP/IP 連接開啟時,運行 Sync Adapter。這個消息是網(wǎng)絡(luò)框架(Networking Framework)的一個基本部分??梢詫⑦@一選項作為自動運行 Sync Adapter 的一個方法。另外還可以考慮將它和基于時間間隔運行 Sync Adapter 的策略結(jié)合起來使用。

每隔一定時間:

可以每隔一段指定的時間間隔后,運行 Sync Adapter,或者在每天的固定時間運行它。

根據(jù)需求:

運行 Sync Adapter 以響應(yīng)用戶的行為。然而,為了提供最佳的用戶體驗,我們應(yīng)該主要依賴那些更加自動式的選項。使用自動式的選項,可以節(jié)省大量的電量以及網(wǎng)絡(luò)資源。

本課程的后續(xù)部分會詳細介紹每個選項。

當(dāng)服務(wù)器數(shù)據(jù)變化時,運行 Sync Adapter

如果我們的應(yīng)用從服務(wù)器傳輸數(shù)據(jù),且服務(wù)器的數(shù)據(jù)會頻繁地發(fā)生變化,那么可以使用一個 Sync Adapter 通過下載數(shù)據(jù)來響應(yīng)服務(wù)端數(shù)據(jù)的變化。要運行 Sync Adapter,我們需要讓服務(wù)端向應(yīng)用的 BroadcastReceiver 發(fā)送一條特殊的消息。為了響應(yīng)這條消息,可以調(diào)用 ContentResolver.requestSync() 方法,向 Sync Adapter 框架發(fā)出信號,讓它運行 Sync Adapter。

谷歌云消息(Google Cloud Messaging,GCM)提供了我們需要的服務(wù)端組件和設(shè)備端組件,來讓上述消息系統(tǒng)能夠運行。使用 GCM 觸發(fā)數(shù)據(jù)傳輸比通過向服務(wù)器輪詢的方式要更加可靠,也更加有效。因為輪詢需要一個一直處于活躍狀態(tài)的 Service,而 GCM 使用的 BroadcastReceiver 僅在消息到達時會被激活。另外,即使沒有更新的內(nèi)容,定期的輪詢也會消耗大量的電池電量,而 GCM 僅在需要時才會發(fā)出消息。

Note:如果我們使用 GCM,將廣播消息發(fā)送到所有安裝了我們的應(yīng)用的設(shè)備,來激活 Sync Adapter。要記住他們會在同一時間(粗略地)收到我們的消息。這會導(dǎo)致在同一時段內(nèi)有多個 Sync Adapter 的實例在運行,進而導(dǎo)致服務(wù)器和網(wǎng)絡(luò)的負載過重。要避免這一情況,我們應(yīng)該考慮為不同的設(shè)備設(shè)定不同的 Sync Adapter 來延遲啟動時間。

下面的代碼展示了如何通過 requestSync() 響應(yīng)一個接收到的 GCM 消息:

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(ACCOUNT, AUTHORITY, null);
            ...
        }
        ...
    }
    ...
}

當(dāng) Content Provider 的數(shù)據(jù)變化時,運行 Sync Adapter

如果我們的應(yīng)用在一個 Content Provider 中收集數(shù)據(jù),并且希望當(dāng)我們更新了 Content Provider 的時候,同時更新服務(wù)器的數(shù)據(jù),我們可以配置 Sync Adapter 來讓它自動運行。要做到這一點,首先應(yīng)該為 Content Provider 注冊一個 Observer。當(dāng) Content Provider 的數(shù)據(jù)發(fā)生了變化之后,Content Provider 框架會調(diào)用 Observer。在 Observer 中,調(diào)用 requestSync() 來告訴框架現(xiàn)在應(yīng)該運行 Sync Adapter 了。

Note:如果我們使用的是一個 Stub Content Provider,那么在 Content Provider 中不會有任何數(shù)據(jù),并且不會調(diào)用 onChange() 方法。在這種情況下,我們不得不提供自己的某種機制來檢測設(shè)備數(shù)據(jù)的變化。這一機制還要負責(zé)在數(shù)據(jù)發(fā)生變化時調(diào)用 requestSync()。

為了給 Content Provider 創(chuàng)建一個 Observer,繼承 ContentObserver 類,并且實現(xiàn) onChange() 方法的兩種形式。在 onChange() 中,調(diào)用 requestSync() 來啟動 Sync Adapter。

要注冊 Observer,需要將它作為參數(shù)傳遞給 registerContentObserver()。在該方法中,我們還要傳遞一個我們想要監(jiān)視的 Content URI。Content Provider 框架會將這個需要監(jiān)視的 URI 與其它一些 Content URIs 進行比較,這些其它的 Content URIs 來自于 ContentResolver 中那些可以修改 Provider 的方法(如 ContentResolver.insert())所傳入的參數(shù)。如果出現(xiàn)了變化,那么我們所實現(xiàn)的 ContentObserver.onChange() 將會被調(diào)用。

下面的代碼片段展示了如何定義一個 ContentObserver,它在表數(shù)據(jù)發(fā)生變化后調(diào)用 requestSync()

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 mUri;
    // 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(ACCOUNT, 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
        mUri = 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(mUri, true, observer);
        ...
    }
    ...
}

在一個網(wǎng)絡(luò)消息之后,運行 Sync Adapter

當(dāng)可以獲得一個網(wǎng)絡(luò)連接時,Android 系統(tǒng)會每隔幾秒發(fā)送一條消息來保持 TCP/IP 連接處于開啟狀態(tài)。這一消息也會傳遞到每個應(yīng)用的 ContentResolver 中。通過調(diào)用 setSyncAutomatically(),我們可以在 ContentResolver 收到消息后,運行 Sync Adapter。

每當(dāng)網(wǎng)絡(luò)消息被發(fā)送后運行 Sync Adapter,通過這樣的調(diào)度方式可以保證每次運行 Sync Adapter 時都可以訪問網(wǎng)絡(luò)。如果不是每次數(shù)據(jù)變化時就要以數(shù)據(jù)傳輸來響應(yīng),但是又希望自己的數(shù)據(jù)會被定期地更新,那么我們可以用這一選項。類似地,如果我們不想要定期執(zhí)行 Sync Adapter,但希望經(jīng)常運行它,我們也可以使用這一選項。

由于 setSyncAutomatically() 方法不會禁用 addPeriodicSync(),所以 Sync Adapter 可能會在一小段時間內(nèi)重復(fù)地被觸發(fā)激活。如果我們想要定期地運行 Sync Adapter,應(yīng)該禁用 setSyncAutomatically()

下面的代碼片段展示如何配置 ContentResolver,利用它來響應(yīng)網(wǎng)絡(luò)消息,從而運行 Sync Adapter:

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";
    // 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 automatic syncing for the default account and authority
        mResolver.setSyncAutomatically(ACCOUNT, AUTHORITY, true);
        ...
    }
    ...
}

定期地運行Sync Adapter

我們可以設(shè)置一個在運行之間的時間間隔來定期運行 Sync Adapter,或者在每天的固定時間運行它,還可以兩種策略同時使用。定期地運行 Sync Adapter 可以讓服務(wù)器的更新間隔大致保持一致。

同樣地,當(dāng)服務(wù)器相對來說比較空閑時,我們可以通過在夜間定期調(diào)用 Sync Adapter,把設(shè)備上的數(shù)據(jù)上傳到服務(wù)器。大多數(shù)用戶在晚上不會關(guān)機,并為手機充電,所以這一方法是可行的。而且,通常來說,設(shè)備不會在深夜運行除了 Sync Adapter 之外的其他的任務(wù)。然而,如果我們使用這個方法的話,我們需要注意讓每臺設(shè)備在略微不同的時間觸發(fā)數(shù)據(jù)傳輸。如果所有設(shè)備在同一時間運行我們的 Sync Adapter,那么我們的服務(wù)器和移動運營商的網(wǎng)絡(luò)將很有可能負載過重。

一般來說,當(dāng)我們的用戶不需要實時更新,而希望定期更新時,使用定期運行的策咯會很有用。如果我們希望在數(shù)據(jù)的實時性和 Sync Adapter 的資源消耗之間進行一個平衡,那么定期執(zhí)行是一個不錯的選擇。

要定期運行我們的 Sync Adapter,調(diào)用 addPeriodicSync()。這樣每隔一段時間,Sync Adapter 就會運行。由于 Sync Adapter 框架會考慮其他 Sync Adapter 的執(zhí)行,并嘗試最大化電池效率,所以間隔時間會動態(tài)地進行細微調(diào)整。同時,如果當(dāng)前無法獲得網(wǎng)絡(luò)連接,框架不會運行我們的 Sync Adapter。

注意,addPeriodicSync() 方法不會讓 Sync Adapter 每天在某個時間自動運行。要讓我們的 Sync Adapter 在每天的某個時刻自動執(zhí)行,可以使用一個重復(fù)計時器作為觸發(fā)器。重復(fù)計時器的更多細節(jié)可以閱讀:AlarmManager。如果我們使用 setInexactRepeating() 方法設(shè)置了一個每天的觸發(fā)時刻會有粗略變化的觸發(fā)器,我們?nèi)匀粦?yīng)該將不同設(shè)備 Sync Adapter 的運行時間隨機化,使得它們的執(zhí)行交錯開來。

addPeriodicSync() 方法不會禁用 setSyncAutomatically(),所以我們可能會在一小段時間內(nèi)產(chǎn)生多個 Sync Adapter 的運行實例。另外,僅有一部分 Sync Adapter 的控制標(biāo)識可以在調(diào)用 addPeriodicSync() 時使用。不被允許的標(biāo)識在該方法的文檔中可以查看。

下面的代碼樣例展示了如何定期執(zhí)行 Sync Adapter:

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(
                ACCOUNT,
                AUTHORITY,
                Bundle.EMPTY,
                SYNC_INTERVAL);
        ...
    }
    ...
}

按需求執(zhí)行 Sync Adapter

以響應(yīng)用戶請求的方式運行 Sync Adapter 是最不推薦的策略。要知道,該框架是被特別設(shè)計的,它可以讓 Sync Adapter 在根據(jù)某個調(diào)度規(guī)則運行時,能夠盡量最高效地使用手機電量。顯然,在數(shù)據(jù)改變的時候執(zhí)行同步可以更有效的使用手機電量,因為電量都消耗在了更新新的數(shù)據(jù)上。

相比之下,允許用戶按照自己的需求運行 Sync Adapter 意味著 Sync Adapter 會自己運行,這將無法有效地使用電量和網(wǎng)絡(luò)資源。如果根據(jù)需求執(zhí)行同步,會誘導(dǎo)用戶即便沒有證據(jù)表明數(shù)據(jù)發(fā)生了變化也請求一個更新,這些無用的更新會導(dǎo)致對電量的低效率使用。一般來說,我們的應(yīng)用應(yīng)該使用其它信號來觸發(fā)一個同步更新或者讓它們定期地去執(zhí)行,而不是依賴于用戶的輸入。

不過,如果我們?nèi)匀幌胍凑招枨筮\行 Sync Adapter,可以將 Sync Adapter 的配置標(biāo)識設(shè)置為手動執(zhí)行,之后調(diào)用 ContentResolver.requestSync() 來觸發(fā)一次更新。

通過下列標(biāo)識來執(zhí)行按需求的數(shù)據(jù)傳輸:

SYNC_EXTRAS_MANUAL

強制執(zhí)行手動的同步更新。Sync Adapter 框架會忽略當(dāng)前的設(shè)置,比如通過 setSyncAutomatically() 方法設(shè)置的標(biāo)識。

SYNC_EXTRAS_EXPEDITED

強制同步立即執(zhí)行。如果我們不設(shè)置此項,系統(tǒng)可能會在運行同步請求之前等待一小段時間,因為它會嘗試將一小段時間內(nèi)的多個請求集中在一起調(diào)度,目的是為了優(yōu)化電量的使用。

下面的代碼片段將展示如何調(diào)用 requestSync() 來響應(yīng)一個按鈕點擊事件:

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 dummy 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);
    }


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號