译者注:
翻译自Android官方文档,其中有地方存疑已经标注出来,如有错误欢迎指正。
如需转载请标明出处:http://blog.csdn.net/javensun/article/details/41984373
在Android设备和web服务器之间同步数据会使你的应用更实用,更吸引用户,例如,将手机数据传到服务端实现数据备份,将数据从服务端取回让用户能够脱机使用。在某些情况下,用户会发现这样会更方便:通过web修改信息然后在手机上就可以继续使用,或者隔一段时间将手机上的数据上传到一个总存储区。
虽然你可以在应用中设计自己的数据传输系统,但也应该考虑一下用Android的sync adater框架。它可以协助管理和自动发起数据传输,也可以协调不同应用的同步操作。使用这个同步框架比自己设计数据传输策略有如下优势:
插件架构:
可以将数据传输的代码以可调用组件的形式添加到系统中。
自动执行:
可以根据不同条件自动发起数据传输,比如数据变更,间隔一定时间,或者是每天定时。而且,系统会将暂时不能运行的操作添加到队列里,在可能的情况下重新发起。
自动检查网络:
系统只会在有网络的情况下发起数据传输
优化电池性能:
可以集中处理数据传输任务。将你的应用的数据传输与其他应用的传输结合,减少系统切换网络的次数,从而降低功耗。
账号管理认证:
如果你的应用需要用户认证功能,你可以选择在数据传输中整合进账号管理认证。
这篇课程将教你如何创建sync adapter并绑定对应的Service,如何提供那些将sync adapter插入到框架中所需的组件,如何用不同的方式运行sync adapter.
创建Authenticator
Sync adapter 框架假设数据传输时是要账号认证的。因此,需要你在sync adapter中提供一个叫authenticator的组件。这个组件将会插入到android的账号认证框架中,并提供管理用户认证信息的标准接口。
即使你的应用不需要账号认证,你还是需要创建一个authenticator组件。如果你不用账号登录,authenticator的信息将被忽略,因此你可以提供一个仅包含了方法实现的组件。还需要提供一个Service给sync adapter框架,以调用authenticator的方法。
接下来将教你如何定义一个满足sync adapter框架需要的虚拟authenticator。如果你需要创建一个真实管理用户账户的authenticator,请参阅
AbstractAccountAuthenticator
的文档。
添加Authenticator 组件
创建一个继承自
AbstractAccountAuthenticator
的类,通过返回null或是抛出异常来模拟实现需要的方法。
下面的代码段是一份样例
/* * Implement AbstractAccountAuthenticator and stub out all * of its methods */ public class Authenticator extends AbstractAccountAuthenticator { // Simple constructor public Authenticator(Context context) { super(context); } // Editing properties is not supported @Override public Bundle editProperties( AccountAuthenticatorResponse r, String s) { throw new UnsupportedOperationException(); } // Don't add additional accounts @Override public Bundle addAccount( AccountAuthenticatorResponse r, String s, String s2, String[] strings, Bundle bundle) throws NetworkErrorException { return null; } // Ignore attempts to confirm credentials @Override public Bundle confirmCredentials( AccountAuthenticatorResponse r, Account account, Bundle bundle) throws NetworkErrorException { return null; } // Getting an authentication token is not supported @Override public Bundle getAuthToken( AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Getting a label for the auth token is not supported @Override public String getAuthTokenLabel(String s) { throw new UnsupportedOperationException(); } // Updating user credentials is not supported @Override public Bundle updateCredentials( AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } // Checking features for the account is not supported @Override public Bundle hasFeatures( AccountAuthenticatorResponse r, Account account, String[] strings) throws NetworkErrorException { throw new UnsupportedOperationException(); } }
将Authenticator绑定至Framework
为了使Sync adapter框架能够访问你的authenticator,你必须为它创建一个服务。这个服务提供了一个binder对象,供框架调用authenticator时,或是在authenticator和框架之间传输数据时使用。
既然框架首次启动这个服务时它就需要访问authenticator,你可以通过在Service的onCreate()里调用authenticator的构造方法将其实例化。
下面的示例代码展示了如何定义这个服务:
/** * A bound Service that instantiates the authenticator * when started. */ public class AuthenticatorService extends Service { ... // Instance field that stores the authenticator object private Authenticator mAuthenticator; @Override public void onCreate() { // Create a new authenticator object mAuthenticator = new Authenticator(this); } /* * When the system binds to this Service to make the RPC call * return the authenticator's IBinder. */ @Override public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } }
添加Authenticator的元数据(Metadata)文件
将authenticator组件添加到账号框架中时,你需要提供描述这个组件的元数据。元数据中声明了你创建的账号类型和一些在需要显示给用户的信息(若账号类型对用户可见)。元数据声明在一个XML文件中,这个文件保存在你应用工程的
/res/xml/
目录中。名字自定义,一般叫做
authenticator.xml
。
XML文件中包含单独一个具有以下属性的
<account-authenticator>
元素:
android:accountType
同步框架要求每个sync adapter有一个账号类型。框架会把账号类型作为识别sync adapter的内部标识。对于需要登录的服务器,账号类型会和账号名一起作为登录信息发送给服务器。
如果你的服务器不要求登录,你也要提供一个账号类型,它标明了你所控制的域,框架用这个账号类型来管理你的sync adapter,但是不发送给服务器。
android:icon
指向一个用做图标的Drawable资源。如果你在
res/xml/syncadapter.xml
设置了
android:userVisible="true”
属性将Sync adapter对用户可见,那必须要提供一个图标资源。它将显示在“设置”应用的“账号”一项中。
android:smallIcon
小版本的图标。依据屏幕的尺寸,它可能在设置中用来代替
android:icon。
android:label
给用户标识账号类型的字符串。在“设置”里和上面的图标显示在一起。
以下的代码段是刚刚创建的authenticator 所对应的XML文件:
<?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="example.com" android:icon="@drawable/ic_launcher" android:smallIcon="@drawable/ic_launcher" android:label="@string/app_name"/>
声明Authenticator
通过之前的几步,我们创建了一个将与authenticator与framework关联的Service。要让系统能认识这个Service,我们需要在应用的manifest文件中的
<application>
下声明一个
<service>
子元素如下:
<service android:name="com.example.android.syncadapter.AuthenticatorService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator"/> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service>
<intent-filter>
设置了通过action
android.accounts.AccountAuthenticator
启动的filter。这个action是由系统发送的。当被触发时,系统会启动你之前提供的封装了你的athtenticator的
AuthenticatorService
。
<meta-data>
声明了authenticator的元数据。通过
android:name
属性将meta-data与认证框架关联。
android:resource
指定刚才创建的元数据文件的名字。
除了authenticator以外, sync adapter还需要一个content provider。
创建Content Provider
设计同步框架是用来与安全易用的Content Privder框架协作的。因此,使用同步框架的应用已经先有一个Content Provider来存储本地数据。如果同步框架要运行你的Sync adapter,而你还没有Content Provider,那应用会崩溃。如果你准备开发一个需要与服务端传输数据的应用,那么强烈推荐你用Content Provider来存储数据。不仅是方便使用SyncAdapter,而且它也具有更好的安全性。
尽管如此,如果你已经将本地数据存储为别的什么格式,你仍然可以使用Sync Adapter来传输数据。 为了满足同步框架对Content Privder的要求,你需要在应用中添加一个虚拟的Content Privder,实现一下必须的方法,可以全部返回null或者0. 添加好之后,你就可以通过Sync Adapter按照你自己的存储形式来传输数据。
如果你应用中已经使用过Content Provider,你就可以跳过接下来这一段课程,直接进行“
创建Sync Adapter”这一节。如果没有,那接下来会教你如何添加一个ContentProvider,这样才能将Sync Adapter添加到框架中。
添加一个Content Provider
继承
ContentProvider
类 ,实现那些必要的方法。如下面这段代码:
/* * Define an implementation of ContentProvider that stubs out * all methods */ public class StubProvider extends ContentProvider { /* * Always return true, indicating that the * provider loaded correctly. */ @Override public boolean onCreate() { return true; } /* * Return an empty String for MIME type */ @Override public String getType() { return new String(); } /* * query() always returns no results * */ @Override public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } /* * insert() always returns null (no URI) */ @Override public Uri insert(Uri uri, ContentValues values) { return null; } /* * delete() always returns "no rows affected" (0) */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } /* * update() always returns "no rows affected" (0) */ public int update( Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
在Manifest中声明Provider
同步框架通过检查应用的manifest文件中的声明来确认应用是否有ContentProvider。要声明Provider,需要在manifest里添加一个
<provider>
属性,它具有以下属性:
android:name="com.example.android.datasync.provider.StubProvider"
指定实现ContentProvider的完整类名。
android:authorities="com.example.android.datasync.provider"
用来唯一指定一个ContentProvider的URI authority。这个值最好设置为“包名 + .provider”。
Even though you're declaring your stub provider to the system, nothing tries to access the provider itself.
android:exported="false"
设置外部应用是否可以访问。因为我们的Provider并不需要别的应用访问,所以设置为”false”。这个值并不影响同步框架的访问。
android:syncable="true"
设置是否可同步。如果设置为true 就不需要在代码中再调用
setIsSyncable()
。这个值决定了同步框架可以与Provider传输数据,但是也仅在你明确调用的时候才传输。(
译者注:SyncAdapter有类似属性:isAlwaysSyncable
)。
以下代码段说明了如何在manifest中声明Provider:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.network.sync.BasicSyncAdapter" android:versionCode="1" android:versionName="1.0" > <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > ... <provider android:name="com.example.android.datasync.provider.StubProvider" android:authorities="com.example.android.datasync.provider" android:exported="false" android:syncable="true"/> ... </application> </manifest>
到此为止,我们已经创建了同步框架所有需要的依赖项。接下来需要创建数据传输的组件,叫做Sync Adapter.
创建SyncAdapter
应用中的SyncAdapter封装了应用和服务器传输数据的代码。同步框架会根据你设置的定期任务或者触发器运行SyncAdapter组件中的代码。要在应用中添加SyncAdapter需要添加如下几点:
- SyncAdapter 类
这个类会将数据传输的代码按照与同步框架兼容的接口进行封装。 - 同步Service
提供给外部进行绑定,让框架可以运行SyncAdapter中的代码。 - SyncAdapter的XML文件
其中包含了SyncAdapter的一些信息,框架会根据这个文件的内容来决定如何加载和安排数据传输。 - 在AndroidManifest中的声明
声明了绑定用的Service,并标明了SyncAdapter的元数据文件。
创建SyncAdapter类
包括继承基类、定义构造方法、实现数据传输的方法。
继承AbstracThreadedSyncAdapter基类
首先要继承
AbstractThreadedSyncAdapter 基类并且实现其构造方法。在
构造方法中做一些创建
SyncAdapter时需要的
设置
,类似于在
Activity.onCreate()
设置一个Activity。例如,如果你使用ContentProvider存储数据的,在构造方法中就去获取一下
ContentResolver
的实例。因为在Android3.0版本中新加了一个支持
parallelSyncs
参数的构造方法,为保持兼容需要创建两种形式的构造方法。
注意: 同步框架是为单实例的SyncAdapter组件而设计的(
译者注:若非单例模式可能会导致内存泄露)
。实例化SyncAdapter的详细内容请看 Bind the Sync Adapter to the Framework.
下面的例子展示了如何实现
AbstractThreadedSyncAdapter
及其构造方法。
/** * Handle the transfer of data between a server and an * app, using the Android sync adapter framework. */ public class SyncAdapter extends AbstractThreadedSyncAdapter { ... // Global variables // Define a variable to contain a content resolver instance ContentResolver mContentResolver; /** * Set up the sync adapter */ public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); /* * If your app uses a content resolver, get an instance of it * from the incoming Context */ mContentResolver = context.getContentResolver(); } ... /** * Set up the sync adapter. This form of the * constructor maintains compatibility with Android 3.0 * and later platform versions */ public SyncAdapter( Context context, boolean autoInitialize, boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); /* * If your app uses a content resolver, get an instance of it * from the incoming Context */ mContentResolver = context.getContentResolver(); ... }
添加数据传输的代码至onPerformSync()
SyncAdapter不会自动做数据传输,它只是封装你的代码,以便框架可以在后台调用,而不需要你的应用介入。同步框架准备要同步应用数据的时候,它会调用你SyncAdapter中实现的
onPerformSync()
方法。
为了方便SyncAdapter获取你应用的数据,同步框架调用
onPerformSync()
时会传入以下参数:
- Account
与本次触发事件关联的Account
对象,如果你的服务器不需要账号,直接无视就可以。 - Extras
包含一些标志位的Bundle
对象 - Authority
系统中ContentProvider的authority,一般是你自己应用中的ContentProvider对应的authority。 - Content provider client
authority对应的ContentProviderClient
,它是ContentProvider的一个轻量接口,具有与ContentResolver
相同的功能。如果是用ContentProvider保存的数据,你可以用这个对象连接到ContentProvider,否则无视就好。 - Sync result
SyncResult
对象,可以用来将同步的结果传给同步框架。
下面的代码段展示了
onPerformSync()
的总体结构:
/* * Specify the code you want to run in the sync adapter. The entire * sync adapter runs in a background thread, so you don't have to set * up your own background processing. */ @Override public void onPerformSync( Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { /* * Put the data transfer code here. */ ... }
当然实际实现的
onPerformSync()
是根据你的同步需求和服务器连接协议定制的,通常是下面几个通用的步骤:
- 连接服务器
虽然同步开始时你可以认为网络是通畅的,但是同步框架并不会自动帮你连接服务器 - 下载上传数据
SyncAdapter不会自动做数据传输。如果你要从服务端取数据存到本地,那你必须提供请求、下载、插入数据的代码。同样,如果需要上传数据,也要读数据、发送数据请求。除此之外,还需要处理数据传输中发生的网络错误。 - 处理数据冲突
SyncAdapter不会自动处理服务端和本地的数据冲突。而且,也不会检测本地和服务端的数据哪一个更新。你必须自己提供算法处理这种场景。 - 清理
在传输结束后关闭与服务器的链接,清理临时文件和缓存。
除了同步相关的任务之外,也应该尽量将网络相关的任务添加到
onPerformSync()
中。将网络操作集中处理可以降低频繁的发起终止网络造成的功耗。要了解如何高效的完成网络访问,可以参阅
Transferring Data Without Draining the Battery,其中讲了几个网络传输任务,可以添加到数据传输的代码中。
将SyncAdapter绑定到framework
现在我们已经有了一个封装了数据传输代码的SyncAdapter,接下来还要把它开放给framework调用,需要创建一个绑定用的Service,并把SyncAdapter的binder对象传给framework。通过这个binder,framework就可以调用到
onPerformSync()
方法。
在Service的
onCreate()
方法中以单实例的形式实例化SyncAdapter,这样也会将SyncAdapter的实例化延迟到framework首次传输数据要创建Service的时候执行。实例化的过程要保证线程安全,以免同步框架会将多次同步响应添加到队列中。
下面的代码段展示了如何实现Service类、实例化SyncAdapter并获取binder对象:
package com.example.android.syncadapter;
/**
* Define a Service that returns an IBinder
for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
public class SyncService extends Service {
// Storage for an instance of the sync adapter
private static SyncAdapter sSyncAdapter = null;
// Object to use as a thread-safe lock
private static final Object sSyncAdapterLock = new Object();
/*
* Instantiate the sync adapter object.
*/
@Override
public void onCreate() {
/*
* Create the sync adapter as a singleton.
* Set the sync adapter as syncable
* Disallow parallel syncs
*/
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
}
}
}
/**
* Return an object that allows the system to invoke
* the sync adapter.
*
*/
@Override
public IBinder onBind(Intent intent) {
/*
* Get the object that allows external processes
* to call onPerformSync(). The object is created
* in the base class code when the SyncAdapter
* constructors call super()
*/
return sSyncAdapter.getSyncAdapterBinder();
}
}
注意: 更详细的示例代码可以参阅示例工程BasicSyncAdapter.zip.
添加Account
同步框架要求每个SyncAdapter必须有一个账号类型,对应之前在“添加Authenticator元数据文件”提到的。现在需要在Android系统中设置这个账号类型:调用
addAccountExplicitly()
方法,添加一个具有账号类型的虚拟账号。最好是在打开应用页面时的
onCreate()
中调用这个方法。下面是示例:
public class MainActivity extends FragmentActivity { ... ... // Constants // The authority for the sync adapter's content provider public static final String AUTHORITY = "com.example.android.datasync.provider" // An account type, in the form of a domain name public static final String ACCOUNT_TYPE = "example.com"; // The account name public static final String ACCOUNT = "dummyaccount"; // Instance fields Account mAccount; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... // Create the dummy account mAccount = CreateSyncAccount(this); ... } ... /** * Create a new dummy account for the sync adapter * * @param context The application context */ public static Account CreateSyncAccount(Context context) { // Create the account type and default account Account newAccount = new Account( ACCOUNT, ACCOUNT_TYPE); // Get an instance of the Android account manager AccountManager accountManager = (AccountManager) context.getSystemService( ACCOUNT_SERVICE); /* * Add the account and account type, no password or user data * If successful, return the Account object, otherwise report an error. */ if (accountManager.addAccountExplicitly(newAccount, null, null))) { /* * If you don't set android:syncable="true" in * in your <provider> element in the manifest, * then call context.setIsSyncable(account, AUTHORITY, 1) * here. */ } else { /* * The account exists or some other error occurred. Log this, report it, * or handle it internally. */ } } ... }
添加SyncAdapter的元数据文件
要将SyncAdapger添加到框架中,需要给框架提供描述SyncAdapter的元数据和一些别的标志位。元数据指定了SyncAdapter的账号类型、对应的ContentProvider的Authority、系统中和SyncAdapter相关的部分UI,和其他的一些同步相关的标志。声明元数据的XML文件保存在
/res/xml/
目录下,名字自定义,但一般叫做
syncadapter.xml
。
这个XML文件包含了一个单独的元素
<sync-adapter>
,它具有以下属性:
- android contentAuthority
之前创建的ContentProvider对应的URI的Authority。如果你的数据是从ContentProvider传给服务端的,那这个要对应ContentProvider的XML中<provider>
元素的android:authorities
属性值。 - android accountType
同步框架所需要的账号类型,需要与前面在代码段中的ACCOUNT_TYPE
常量还有authenticator的元数据文件中定义的保持一致。 - android:userVisible
设置SyncAdapter的账号类型是否在“设置”应用可见。默认情况下账号的图标和标签是在“设置”中可见的。除非你有与应用关联的账号类型或域,否则还是将它设为不可见。即使设置为不可见,还是可以让用户在应用内的界面对SyncAdapter进行控制。 - android:supportsUploading
允许应用上传数据到云端。如果你的应用只需要下载数据,那就设置为false。(译者注:当发起同步时设置了ContentResolver.SYNC_EXTRAS_UPLOAD,这个属性才起作用,用来决定是否支持仅上传的操作。ContentResolver中的notifyChange(android.net.Uri, android.database.ContentObserver, boolean)
方法发起的同步会带仅上传的SYNC_EXTRAS_UPLOAD标志位) - android:allowParallelSyncs
设置是否允许SyncAdapter多实例同时运行。如果你的应用需要支持多账号并发传输数据时才需要使用这个标志,如果没有并发的数据传输,那这个标志是无效的。 - android:isAlwaysSyncable
指定同步框架是否可以在任意时间运行你的SyncAdapter。如果你只希望通过程序控制何时发起,那就设置为false,然后通过调用requestSync()
发起。了解更多请参看Running a Sync Adapter;(译者注:Provider中有isSyncable属性,功能类似)
下面的代码展示了使用一个虚拟账号仅作下载的SyncAdapter的XML文件内容:
<?xml version="1.0" encoding="utf-8"?> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:contentAuthority="com.example.android.datasync.provider" android:accountType="com.android.example.datasync" android:userVisible="false" android:supportsUploading="false" android:allowParallelSyncs="false" android:isAlwaysSyncable="true"/>
在AndroidManifest中声明SyncAdapter
创建了SyncAdapter之后,就要请求相关的权限并声明绑定用的Service。因为SyncAdapter是在本地与网络传输数据的,就需要申请访问互联网的权限。除此之外,为了在程序中控制SyncAdapter,还需要申请读写SyncAdapter设置的权限。还有允许使用之前创建的authenticator组件的权限
申请这些权限需要在
<manifest>
元素里添加以下子元素:
android.permission.INTERNET
允许访问互联网上传下载数据。android.permission.INTERNET
允许读取当前SyncAdapter的配置。例如调用getIsSyncable()
方法时就需要这个权限。android.permission.WRITE_SYNC_SETTINGS
允许写SyncAdapter的配置。使用addPeriodicSync()
方法时就需要这个权限。但调用requestSync()
发起同步并不需要这个权限。android.permission.AUTHENTICATE_ACCOUNTS
允许使用你创建的Authenticator。
以下的代码段展示了如何添加这些权限:
<manifest> ... <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/> ... </manifest>
最后,要声明Framework与SyncAdapter交互用的Service,在
<application>
中添加下面的子元素:
<service android:name="com.example.android.datasync.SyncService" android:exported="true" android:process=":sync"> <intent-filter> <action android:name="android.content.SyncAdapter"/> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter" /> </service>
<intent-filter>
元素设置了一个由Action 为
android.content.SyncAdapter
的Intent触发的过滤器,系统要运行SyncAdapter时会发送这个Intent。当过滤器被触发时,系统会创建绑定用的Service,本例中即
SyncService
。
android:exported="true"
的属性允许出你的应用之外的进程来访问这个Service。
android:process=":sync"
告诉系统在名为
sync
的全局共享的进程中运行这个Service。如果你的应用中有多个SyncAdapter,他们可以共享这个进程,可以降低一些消耗。
<meta-data>
元素规定了我们之前创建SyncAdapter元数据XML文件。
android:name
属性说明了这个元数据是给同步框架的。
android:resource
元素指定了元数据文件的名字。
现在我们准备好了SyncAdapter所有的组件。下面来看一下如何告诉同步框架来运行你的SyncAdapter,通过事件响应的方式或是定时触发。
运行SyncAdapter
前面我们讲了如何创建一个封装数据传输代码的SyncAdapter,如何添加其它组件将SyncAdapter加入到系统。至此我们要安装一个带有SyncAdapter的应用已经万事俱备了,但是还并没有看到如何运行SyncAdapter。
你应该通过定期任务或是根据一些事件的结果来运行SyncAdapter。比如,隔一段时间或在每天某个特殊的时间运行,或是在本地数据变化后运行。
但要避免根据用户直接的操作来运行,因为这样的话就难以发挥同步框架管理的优势。比如,不要在UI中提供刷新按钮。
你可以用以下几种方式运行SyncAdapter:
- 服务端数据变化时
服务端数据变化时,根据服务端发送的消息运行。这样可以避免轮询服务器影响性能和功耗。 - 本地数据变化时
本地数据变化后同步可以将本地变化的数据发送到服务端,适合用来确保服务端数据最新。如果数据真的是用ContentProvider保存的,那这种方式是很容易实现的(译者注:在ContentProvider中使用ContentResolver的notifyChange(android.net.Uri, android.database.ContentObserver, boolean)
方法);如果是伪造的ContentProvider,那可能要麻烦一些。 - 系统发送网络消息时
当系统发出保持TCP/IP连接开启的网络消息时发起,这个网络消息是网络框架的一部分。这是自动同步的一种方式,可以考虑和基于时间间隔的同步结合起来使用。 - 固定时间间隔
自定义一个固定的时间间隔,或者是每天的某个时间点发起 - 即时发起
由用户手动操作发起。但是,为了有更好的体验,最好还是以自动同步为主,这样可以降低电池和网络资源的消耗。
接下来的课程进一步讲述了这几种运行方式。
服务端数据变更时同步
如果你的应用是从服务端取数据的,并且服务端数据变更频繁,就可以在数据变更后从服务端下载。要这么做,首先要服务端发送一条特殊的消息到你应用的
BroadcastReceiver,然后应用调用
ContentResolver.requestSync()
发起同步。谷歌云消息(
Google Cloud Messaging
)提供了发送这种消息的服务端和客户端的组件。使用GCM比轮询服务器更可靠更有效率,轮询需要
保持
一个一直
活动的
Service
,而GCM使用仅在消息到达时才被激活的
BroadcastReceiver
;即使服务端没有变更,轮询还是要定期的消耗电量,而GCM仅在需要的时候才会发送消息。
注意:如果通过GCM发送广播给所有安装了应用的设备来发起同步,那这些设备差不多会在同时收到消息。这样会同时有多个SyncAdapter实例运行,导致服务器和网络过载。为了避免这种情况,应该在这些设备上各自做不同期限的延时。
下面的代码段展示了如何在收到GCM消息时运行
requestSync()
发起同步:
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); ... } ... } ... }
ContentProvider数据变化时同步
如果你应用的数据是用ContentProvider管理的,并且希望数据更新的时候也同步到服务端,那就可以设置应用自动同步。首先,需要为ContentProvider注册一个Observer,当ContentProvider数据变化后,框架层会调用这个Observer。在Observer中调用
requestSync()
告诉框架运行sync adapter。
注意: 如果你使用的是伪造的content provider,因为provider中没有数据,那
onChange()
永远不会被调用到。这时,你需要自己提供检测数据变化的机制,然后在数据变化时调用requestSync()
;
要给你的ContentProvider创建Observer,只要继承
ContentObserver
类并且实现其中的
onChange()
方法。 在
onChange()
中调用
requestSync()
来运行SyncAdapter。
注册一个Observer,将它作为参数传入
registerContentObserver(),在
调用时还需要传入需要监听的URI。ContentProvider框架会将这个URI与传入
ContentResolver
的方法(例如
ContentResolver.insert()
)的URI进行比较。如果匹配成功,那你的
ContentObserver.onChange()
会被调用。
下面的一段代码展示了如何定义一个在表数据变化时调用
requestSync()
的
ContentObserver:
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); ... } ... }
收到网络消息后发起同步(????存疑)
译者注:setSyncAutomatically()实际使用中被用作自动同步开关,在本地Provider中notifyChange时和定期同步时都会判断这个开关状态,与本段中文档描述的不一致。
当网络连接可用时,Android系统会每隔几秒钟发送一条消息来保持手机的TCP/IP连接打开。这个消息也会到达每个应用的
ContentResolver
。通过调用setSyncAutomatically()
,可以设置ContentResolve
在收到消息时自动发起同步。
通过设置在收到网络消息时发起同步,能确保在网络可用时发起同步。 如果你不需要在数据变化时立即强制发起同步,但是又想要保证数据有规律的更新,则可以使用这个选项。类似的,如果你不希望按照固定的时间间隔来同步但又希望能够频繁点的同步的话,也可以使用这个选项。
由于
setSyncAutomatically()
不会禁用addPeriodicSync()
,因此你的SyncAdapter可能会被频繁的重复触发。因此如果要定期的触发同步,就应该禁用掉 setSyncAutomatically()
。
下面的代码段展示了如何配置
ContentResolver
通过网络消息触发同步:
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); ... } ... }
定期同步
设置固定的间隔时间,或者设置在每天的某个固定的时间点触发同步,或者两者同时使用。通过定期同步可以粗略的匹配服务端
数据的更新间隔。
同样,你也可以设置晚上同步,在服务端相对空闲的时间段上传数据。大多数用户在晚上会保持开机并充电的状态,因此这个时间点通常可用。而且,手机在运行你的SyncAdapter时不会运行其他的任务。尽管如此,如果要使用这种方式,一定要保证每个设备触发同步的时间有些许不同,试想如果所有的设备同时发起同步,你的服务器和运营商网络恐怕都会过载。
一般情况下,如果你的用户不需要即时更新数据,但又期望能够有规律的更新,那定期同步就派上用场了。还有一种情况,如果你想在数据更新的可用性和SyncAdapter运行效率之间做一个平衡,不想过度消耗资源的话,定期同步也是个不错的选择。
通过调用
addPeriodicSync()
可以设置一个固定的时间间隔,在经过这段时间后发起同步。由于同步框架也负责其他SyncAdapter的执行,为了最大化电池续航,这个同步的间隔时间可能会有几秒的误差。而且,在没有网络的时候,框架也不会发起同步。
注意
addPeriodicSync()
并不能在一天的某个特定时间点发起同步,要实现这一点需要使用重复的闹钟作触发。关于重复闹钟更详细的信息在的
AlarmManager
参考文档中有描述。如果你是调用的
setInexactRepeating()
设置触发时间,那么时间要设置成随机的,这样能保证将不同设备的同步时间错开。
addPeriodicSync()
并不会禁用
setSyncAutomatically()
,因此可能会有多个同步在短时间内发生。而且,只有少数的Flag可以在
addPeriodicSync()
中使用,其他不允许使用的Flag可以参考
addPeriodicSync()
的文档。
以下代码段展示了如何设置定期同步:
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); ... } ... }
按需同步
通过响应用户请求发起同步是最不建议的同步策略。同步框架专门设计了按计划同步的策略来节省功耗。在数据变化时发起同步才是高效的做法,因为电量确实消耗在了传输新数据上。
与之相比,允许用户按需同步意味着同步是被自己发起的,会导致用户在并没有数据变化时发起同步,这种并没有刷新任何数据的同步对网络与电量资源的使用都是低效的。一般情况下,应用尽量使用一些其他的信号来触发同步,或者使用定期同步,而不是通过用户交互来触发。
尽管如此,如果你还是要按需发起同步,那就设置手动同步的flag,然后调用
ContentResolver.requestSync()
。
按需同步使用以下flag:
强制发起手动同步,同步框架会忽略当前的一些设置,比如自动同步开关状态。
强制立即发起同步。如果不设置这个选项,系统为了优化功耗可能会等待几秒钟,将一段时间内的几次同步合并发起。
以下代码展示了如何通过响应按键来发起同步:
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); }
附:
Android同步框架发起同步判断条件
所有同步发起时会判断以下属性:
- Provider的isSyncable
- SyncAdapter的isAlwaysSyncable
自动同步(SYNC_EXTRAS_MANUAL 为false)发起时除以上之外,还要判断以下:
- 系统总同步开关( getMasterSyncAutomatically)
- SyncAdapter同步开关( getSyncAutomatically)
若是通过调用ContentResolver 的notifyChange发起自动同步时会带SYNC_EXTRAS_UPLOAD标志,Android设计原意是仅将本地数据更新至服务端。此时SyncAdapter中的supportsUploading若是false,则不能发起自动同步。