执行 Sync Adpater

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

在本节课之前,我们已经学习了如何创建一个封装了数据传输代码的 Sync Adapter 组件,以及如何添加其它的组件,使得我们可以将 Sync Adapter 集成到系统当中。现在我们已经拥有了所有部件,来安装一个包含有 Sync Adapter 的应用了,但是这里还没有任何代码是负责去运行 Sync Adapter。

执行 Sync Adapter 的时机,一般应该基于某个计划任务或者一些事件的间接结果。例如,我们可能希望 Sync Adapter 以一个定期计划任务的形式运行(比如每隔一段时间或者在每天的一个固定时间运行)。或者也可能希望当设备上的数据发生变化后,执行 Sync Adapter。我们应该避免将运行 Sync Adapter 作为用户某个行为的直接结果,因为这样做的话我们就无法利用 Sync Adapter 框架可以按计划调度的特性。例如,我们应该在 UI 中避免使用刷新按钮。

下列情况可以作为运行 Sync Adapter 的时机:

当服务端数据变更时:

当服务端发送消息告知服务端数据发生变化时,运行 Sync Adapter 以响应这一来自服务端的消息。这一选项允许从服务器更新数据到设备上,该方法可以避免由于轮询服务器所造成的执行效率下降,或者电量损耗。

当设备的数据变更时:

当设备上的数据发生变化时,运行 Sync Adapter。这一选项允许我们将修改后的数据从设备发送给服务器。如果需要保证服务器端一直拥有设备上最新的数据,那么这一选项非常有用。如果我们将数据存储于 Content Provider,那么这一选项的实现将会非常直接。如果使用的是一个 Stub Content Provider,检测数据的变化可能会比较困难。

当系统发送了一个网络消息:

当 Android 系统发送了一个网络消息来保持 TCP/IP 连接开启时,运行 Sync Adapter。这个消息是网络框架(Networking Framework)的一个基本部分。可以将这一选项作为自动运行 Sync Adapter 的一个方法。另外还可以考虑将它和基于时间间隔运行 Sync Adapter 的策略结合起来使用。

每隔一定时间:

可以每隔一段指定的时间间隔后,运行 Sync Adapter,或者在每天的固定时间运行它。

根据需求:

运行 Sync Adapter 以响应用户的行为。然而,为了提供最佳的用户体验,我们应该主要依赖那些更加自动式的选项。使用自动式的选项,可以节省大量的电量以及网络资源。

本课程的后续部分会详细介绍每个选项。

当服务器数据变化时,运行 Sync Adapter

如果我们的应用从服务器传输数据,且服务器的数据会频繁地发生变化,那么可以使用一个 Sync Adapter 通过下载数据来响应服务端数据的变化。要运行 Sync Adapter,我们需要让服务端向应用的BroadcastReceiver发送一条特殊的消息。为了响应这条消息,可以调用ContentResolver.requestSync()方法,向 Sync Adapter 框架发出信号,让它运行 Sync Adapter。

谷歌云消息(Google Cloud Messaging,GCM)提供了我们需要的服务端组件和设备端组件,来让上述消息系统能够运行。使用 GCM 触发数据传输比通过向服务器轮询的方式要更加可靠,也更加有效。因为轮询需要一个一直处于活跃状态的Service,而 GCM 使用的BroadcastReceiver仅在消息到达时会被激活。另外,即使没有更新的内容,定期的轮询也会消耗大量的电池电量,而 GCM 仅在需要时才会发出消息。

Note:如果我们使用 GCM,将广播消息发送到所有安装了我们的应用的设备,来激活 Sync Adapter。要记住他们会在同一时间(粗略地)收到我们的消息。这会导致在同一时段内有多个 Sync Adapter 的实例在运行,进而导致服务器和网络的负载过重。要避免这一情况,我们应该考虑为不同的设备设定不同的 Sync Adapter 来延迟启动时间。

下面的代码展示了如何通过requestSync()响应一个接收到的 GCM 消息:

publicclassGcmBroadcastReceiverextendsBroadcastReceiver{    ...// Constants// Content provider authoritypublicstaticfinalString AUTHORITY ="com.example.android.datasync.provider"// Account typepublicstaticfinalString ACCOUNT_TYPE ="com.example.android.datasync";// AccountpublicstaticfinalString ACCOUNT ="default_account";// Incoming Intent key for extended datapublicstaticfinalString KEY_SYNC_REQUEST ="com.example.android.datasync.KEY_SYNC_REQUEST";    ...@OverridepublicvoidonReceive(Context context, Intent intent){// Get a GCM object instanceGoogleCloudMessaging gcm =                GoogleCloudMessaging.getInstance(context);// Get the type of GCM messageString 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);            ...        }        ...    }    ...}

当 Content Provider 的数据变化时,运行 Sync Adapter

如果我们的应用在一个 Content Provider 中收集数据,并且希望当我们更新了 Content Provider 的时候,同时更新服务器的数据,我们可以配置 Sync Adapter 来让它自动运行。要做到这一点,首先应该为 Content Provider 注册一个 Observer。当 Content Provider 的数据发生了变化之后,Content Provider 框架会调用 Observer。在 Observer 中,调用requestSync()来告诉框架现在应该运行 Sync Adapter 了。

Note:如果我们使用的是一个 Stub Content Provider,那么在 Content Provider 中不会有任何数据,并且不会调用onChange()方法。在这种情况下,我们不得不提供自己的某种机制来检测设备数据的变化。这一机制还要负责在数据发生变化时调用requestSync()

为了给 Content Provider 创建一个 Observer,继承ContentObserver类,并且实现onChange()方法的两种形式。在onChange()中,调用requestSync()来启动 Sync Adapter。

要注册 Observer,需要将它作为参数传递给registerContentObserver()。在该方法中,我们还要传递一个我们想要监视的 Content URI。Content Provider 框架会将这个需要监视的 URI 与其它一些 Content URIs 进行比较,这些其它的 Content URIs 来自于ContentResolver中那些可以修改 Provider 的方法(如ContentResolver.insert())所传入的参数。如果出现了变化,那么我们所实现的ContentObserver.onChange()将会被调用。

下面的代码片段展示了如何定义一个ContentObserver,它在表数据发生变化后调用requestSync()

publicclassMainActivityextendsFragmentActivity{    ...// Constants// Content provider schemepublicstaticfinalString SCHEME ="content://";// Content provider authoritypublicstaticfinalString AUTHORITY ="com.example.android.datasync.provider";// Path for the content provider tablepublicstaticfinalString TABLE_PATH ="data_table";// AccountpublicstaticfinalString ACCOUNT ="default_account";// Global variables// A content URI for the content provider's data tableUri mUri;// A content resolver for accessing the providerContentResolver mResolver;    ...publicclassTableObserverextendsContentObserver{/*

* Define a method that's called when data in the

* observed content provider changes.

* This method signature is provided for compatibility with

* older platforms.

*/@OverridepublicvoidonChange(booleanselfChange){/*

* 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.

*/@OverridepublicvoidonChange(booleanselfChange, 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 =newTableObserver(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);        ...    }    ...}

在一个网络消息之后,运行 Sync Adapter

当可以获得一个网络连接时,Android 系统会每隔几秒发送一条消息来保持 TCP/IP 连接处于开启状态。这一消息也会传递到每个应用的ContentResolver中。通过调用setSyncAutomatically(),我们可以在ContentResolver收到消息后,运行 Sync Adapter。

每当网络消息被发送后运行 Sync Adapter,通过这样的调度方式可以保证每次运行 Sync Adapter 时都可以访问网络。如果不是每次数据变化时就要以数据传输来响应,但是又希望自己的数据会被定期地更新,那么我们可以用这一选项。类似地,如果我们不想要定期执行 Sync Adapter,但希望经常运行它,我们也可以使用这一选项。

由于setSyncAutomatically()方法不会禁用addPeriodicSync(),所以 Sync Adapter 可能会在一小段时间内重复地被触发激活。如果我们想要定期地运行 Sync Adapter,应该禁用setSyncAutomatically()

下面的代码片段展示如何配置ContentResolver,利用它来响应网络消息,从而运行 Sync Adapter:

publicclassMainActivityextendsFragmentActivity{    ...// Constants// Content provider authoritypublicstaticfinalString AUTHORITY ="com.example.android.datasync.provider";// AccountpublicstaticfinalString ACCOUNT ="default_account";// Global variables// A content resolver for accessing the providerContentResolver mResolver;    ...@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        ...// Get the content resolver for your appmResolver = getContentResolver();// Turn on automatic syncing for the default account and authoritymResolver.setSyncAutomatically(ACCOUNT, AUTHORITY,true);        ...    }    ...}

定期地运行Sync Adapter

我们可以设置一个在运行之间的时间间隔来定期运行 Sync Adapter,或者在每天的固定时间运行它,还可以两种策略同时使用。定期地运行 Sync Adapter 可以让服务器的更新间隔大致保持一致。

同样地,当服务器相对来说比较空闲时,我们可以通过在夜间定期调用 Sync Adapter,把设备上的数据上传到服务器。大多数用户在晚上不会关机,并为手机充电,所以这一方法是可行的。而且,通常来说,设备不会在深夜运行除了 Sync Adapter 之外的其他的任务。然而,如果我们使用这个方法的话,我们需要注意让每台设备在略微不同的时间触发数据传输。如果所有设备在同一时间运行我们的 Sync Adapter,那么我们的服务器和移动运营商的网络将很有可能负载过重。

一般来说,当我们的用户不需要实时更新,而希望定期更新时,使用定期运行的策咯会很有用。如果我们希望在数据的实时性和 Sync Adapter 的资源消耗之间进行一个平衡,那么定期执行是一个不错的选择。

要定期运行我们的 Sync Adapter,调用addPeriodicSync()。这样每隔一段时间,Sync Adapter 就会运行。由于 Sync Adapter 框架会考虑其他 Sync Adapter 的执行,并尝试最大化电池效率,所以间隔时间会动态地进行细微调整。同时,如果当前无法获得网络连接,框架不会运行我们的 Sync Adapter。

注意,addPeriodicSync()方法不会让 Sync Adapter 每天在某个时间自动运行。要让我们的 Sync Adapter 在每天的某个时刻自动执行,可以使用一个重复计时器作为触发器。重复计时器的更多细节可以阅读:AlarmManager。如果我们使用setInexactRepeating()方法设置了一个每天的触发时刻会有粗略变化的触发器,我们仍然应该将不同设备 Sync Adapter 的运行时间随机化,使得它们的执行交错开来。

addPeriodicSync()方法不会禁用setSyncAutomatically(),所以我们可能会在一小段时间内产生多个 Sync Adapter 的运行实例。另外,仅有一部分 Sync Adapter 的控制标识可以在调用addPeriodicSync()时使用。不被允许的标识在该方法的文档中可以查看。

下面的代码样例展示了如何定期执行 Sync Adapter:

publicclassMainActivityextendsFragmentActivity{    ...// Constants// Content provider authoritypublicstaticfinalString AUTHORITY ="com.example.android.datasync.provider";// AccountpublicstaticfinalString ACCOUNT ="default_account";// Sync interval constantspublicstaticfinallongSECONDS_PER_MINUTE =60L;publicstaticfinallongSYNC_INTERVAL_IN_MINUTES =60L;publicstaticfinallongSYNC_INTERVAL =            SYNC_INTERVAL_IN_MINUTES *            SECONDS_PER_MINUTE;// Global variables// A content resolver for accessing the providerContentResolver mResolver;    ...@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        ...// Get the content resolver for your appmResolver = getContentResolver();/*

* Turn on periodic syncing

*/ContentResolver.addPeriodicSync(                ACCOUNT,                AUTHORITY,                Bundle.EMPTY,                SYNC_INTERVAL);        ...    }    ...}

按需求执行 Sync Adapter

以响应用户请求的方式运行 Sync Adapter 是最不推荐的策略。要知道,该框架是被特别设计的,它可以让 Sync Adapter 在根据某个调度规则运行时,能够尽量最高效地使用手机电量。显然,在数据改变的时候执行同步可以更有效的使用手机电量,因为电量都消耗在了更新新的数据上。

相比之下,允许用户按照自己的需求运行 Sync Adapter 意味着 Sync Adapter 会自己运行,这将无法有效地使用电量和网络资源。如果根据需求执行同步,会诱导用户即便没有证据表明数据发生了变化也请求一个更新,这些无用的更新会导致对电量的低效率使用。一般来说,我们的应用应该使用其它信号来触发一个同步更新或者让它们定期地去执行,而不是依赖于用户的输入。

不过,如果我们仍然想要按照需求运行 Sync Adapter,可以将 Sync Adapter 的配置标识设置为手动执行,之后调用ContentResolver.requestSync()来触发一次更新。

通过下列标识来执行按需求的数据传输:

SYNC_EXTRAS_MANUAL

强制执行手动的同步更新。Sync Adapter 框架会忽略当前的设置,比如通过setSyncAutomatically()方法设置的标识。

SYNC_EXTRAS_EXPEDITED

强制同步立即执行。如果我们不设置此项,系统可能会在运行同步请求之前等待一小段时间,因为它会尝试将一小段时间内的多个请求集中在一起调度,目的是为了优化电量的使用。

下面的代码片段将展示如何调用requestSync()来响应一个按钮点击事件:

publicclassMainActivityextendsFragmentActivity{    ...// Constants// Content provider authoritypublicstaticfinalString AUTHORITY ="com.example.android.datasync.provider"// Account typepublicstaticfinalString ACCOUNT_TYPE ="com.example.android.datasync";// AccountpublicstaticfinalString ACCOUNT ="default_account";// Instance fieldsAccount mAccount;    ...@OverrideprotectedvoidonCreate(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    *    *@paramv The View associated with the method call,    * in this case a Button    */publicvoidonRefreshButtonClick(View v){        ...// Pass the settings flags by inserting them in a bundleBundle settingsBundle =newBundle();        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);    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容