注意:对于大多数后台处理用例,我们建议使用 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 与作为参数传递给 ContentResolver
方法的内容 URI 进行比较,该方法(例如 ContentResolver.insert()
)会修改提供程序。如果匹配,则调用 ContentObserver.onChange()
实现。
以下代码段展示了如何定义在表格更改时调用 requestSync()
的 ContentObserver
:
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 dummy 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 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); }