运行同步适配器

注意: 我们推荐使用 WorkManager 作为大多数后台处理用例的推荐解决方案。请参考 后台处理指南 了解哪种解决方案最适合您。

在本课程的前几课中,您学习了如何创建封装数据传输代码的同步适配器组件,以及如何添加允许您将同步适配器插入系统的附加组件。您现在拥有安装包含同步适配器的应用程序所需的一切,但是您所看到的代码都没有实际运行同步适配器。

您应该尝试根据计划或作为某些事件的间接结果运行同步适配器。例如,您可能希望您的同步适配器定期运行,例如在特定时间段后或在一天中的特定时间。您可能还希望在设备上存储的数据发生更改时运行同步适配器。您应该避免将同步适配器作为用户操作的直接结果运行,因为这样做您无法充分利用同步适配器框架的调度功能。例如,您应该避免在用户界面中提供刷新按钮。

您可以使用以下选项运行同步适配器

当服务器数据更改时
响应来自服务器的消息运行同步适配器,指示基于服务器的数据已更改。此选项允许您刷新从服务器到设备的数据,而不会因轮询服务器而降低性能或浪费电池寿命。
当设备数据更改时
在设备上的数据发生更改时运行同步适配器。此选项允许您将修改后的数据从设备发送到服务器,并且如果您需要确保服务器始终具有最新的设备数据,这一点尤其有用。如果您实际在内容提供者中存储数据,此选项易于实现。如果您使用的是存根内容提供者,则检测数据更改可能更困难。
定期
在您选择的间隔到期后运行同步适配器,或每天在特定时间运行。
按需
响应用户操作运行同步适配器。但是,为了提供最佳的用户体验,您应该主要依赖于更自动化的选项之一。通过使用自动选项,您可以节省电池和网络资源。

本课的其余部分将更详细地描述每个选项。

当服务器数据更改时运行同步适配器

如果您的应用程序从服务器传输数据,并且服务器数据频繁更改,您可以使用同步适配器来响应数据更改进行下载。要运行同步适配器,让服务器向应用程序中的 BroadcastReceiver 发送一条特殊消息。响应此消息,调用 ContentResolver.requestSync() 以向同步适配器框架发出信号以运行您的同步适配器。

Google Cloud Messaging (GCM) 提供您使此消息系统正常工作所需的服务器和设备组件。使用 GCM 触发传输比轮询服务器状态更可靠、更高效。虽然轮询需要始终处于活动状态的 Service,但 GCM 使用在消息到达时激活的 BroadcastReceiver。虽然定期轮询即使没有更新可用也会消耗电池电量,但 GCM 仅在需要时发送消息。

注意: 如果您使用 GCM 通过广播到安装了应用程序的所有设备来触发同步适配器,请记住,它们几乎在同一时间收到您的消息。这种情况会导致您的同步适配器的多个实例同时运行,导致服务器和网络过载。为了避免这种情况广播到所有设备,您应该考虑将同步适配器的启动延迟一段时间,该时间段对于每个设备来说都是唯一的。

以下代码段向您展示了如何在响应传入 GCM 消息时运行 requestSync()

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() 时将其作为参数传递。在此调用中,您还必须传递要观察的数据的 content URI。内容提供程序框架会将此观察 URI 与作为 ContentResolver 方法参数传递的 content URI 进行比较,这些方法会修改您的提供程序,例如 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);
    }