参考资料:
- 官方文档
- csdn博客
SyncAdapter是什么?
SyncManager是Android提供的一个同步框架,该框架实施了许多最佳做法,它允许Android应用使用Google应用中实现高效同步的一个基本框架。
它实际上是一个数据集中点,将所有的数据传输都放到同一个地方,以便操作系统智能地安排数据传输,优化电池性能。
你可以通过SyncAdapter来使用该框架处理同步请求。
为什么使用SyncAdapter?
SyncAdapter可以智能安排数据传输,如检查网络连接、下载失败后重试等。可以根据不同条件自动发起数据传输,如服务器数据变更、定时同步等。
使用SyncAdapter可以加快应用的加载时间、实现离线功能,可以在数据及时同步和减少网络调用以节约电池电量之间达到一种平衡局面。
何时使用SyncAdapter?
SyncAdapter适用于需要同步本地数据和在线账户信息的应用,如电子邮件的定时收取、笔记应用的云备份、天气应用的及时同步等。
SyncAdapter设计为必须与用户账户绑定,即使你的应用不需要账户认证,也需要实现相关的类来处理账户,并可以将其隐藏。
如何创建SyncAdapter?
1. 创建Authenticator类
该类继承了AbstractAccountAuthenticator
类,用于管理账户认证。如果你的应用不需要账户认证,可以提供一个仅包含方法实现的类,Authenticator的信息将被忽略。
以下是Android提供的无需账户认证的样例代码,如果需要创建真实管理用户账户的Authenticator,请参阅AbstractAccountAuthenticator文档。
/*
* 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();
}
}
2. 创建AuthenticatorService服务
此服务提供给SyncAdapter framework,用于调用Authenticator的方法。
在onCreat()
中创建Authenticator对象,在onBind()
中返回一个binder对象用于在Authenticator和framework间传输数据。
下面是Android提供的一个样例代码:
/**
* 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();
}
}
3. 添加Authenticator的元数据文件
元数据写在一个xml文件中,用于声明账户类型和一些显示给用户的信息,保存在/res/xml/
目录。
文件名自定义,一般定义为authenticator.xml
,根标签为
,一般有以下属性:
-
android:accountType
framework把账户类型作为识别SyncAdapter的内部标识,对于需要验证账户的应用,账户类型会和账户名一起发送给服务器进行验证;对于不需要验证的应用,也要提供账户类型,用于标明一个控制域,framework用这个账户类型类管理你的SyncAdapter,但是不会发送给服务器。 -
android:icon
指向用做图标的Drawable资源。如果在res/xml/syncadapter.xml 设置了android:userVisible="true” 属性将Sync adapter对用户可见,则必须要提供一个图标资源。它将显示在“设置”应用的“账号”一项中。 -
android:smallIcon
小图标,根据屏幕尺寸可能在设置中代替icon属性。 -
android:label
标识账户类型,一般为应用名。
以下为样例代码:
4. 在清单文件中声明AuthenticatorService
样例代码:
设置了通过actionandroid.accounts.AccountAuthenticator
启动的filter。这个action是由系统发送的。当被触发时,系统会启动封装了你的Authtenticator的AuthenticatorService。
声明了authenticator的元数据。通过android:name 属性将meta-data与认证框架关联。android:resource
指定元数据文件。
5. 创建ContentProvider
SyncManager同步框架被设计用来与ContentProvider框架协作,需要一个ContentProvider来存储本地数据。使用ContentProvider的好处与其创建方法此处不再细述,之后会在另一篇笔记中记录。
如果你已经将本地数据存储为别的格式,无法实现ContentProvider,那么可以创建一个虚拟的ContentProvider,实现必要的方法返回null或0。之后可以通过SyncAdapter按照自己的方式传输数据。
虚拟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 no type for MIME type
*/
@Override
public String getType(Uri uri) {
return null;
}
/*
* 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;
}
}
6. 在清单文件中声明ContentProvider
provider的声明方法不再细述,主要是添加android:syncable
属性为true
,使其支持同步。
至此完成了同步框架所需要的全部依赖项。
7. 创建SyncAdapter类
- 继承
AbstractThreadedSyncAdapter
基类,在构造方法中做一些初始化设置,如获取ContentResolver实例等。 - 在
onPerformSync()
方法中添加数据传输的代码,框架将自动将其放在后台线程中运行。除同步相关任务外,也应将网络相关的任务放在此处,将网络操作集中处理可以降低频繁发起网络的功耗。 - 可添加辅助方法
syncImmediately()
,调用此方法来立即执行同步,可用于“刷新”操作:public static void syncImmediately(Context context) { Bundle bundle = new Bundle(); //将此同步放在同步请求队列前面,立即进行同步而不延迟 bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); //忽略当前设置强制发起同步 bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); ContentResolver.requestSync(getSyncAccount(context), context.getString(R.string.content_authority), bundle); }
- 由于请求同步时需要一个同步账户,可添加辅助方法
getSyncAccount()
来获取账户。
此方法与第九步添加Account的功能相同,只是将代码放在了同步框架代码中。
提供一个虚拟账户的示例:public static Account getSyncAccount(Context context) { // Get an instance of the Android account manager AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); // Create the account type and default account Account newAccount = new Account( context.getString(R.string.app_name), context.getString(R.string.sync_account_type)); // If the password doesn't exist, the account doesn't exist if ( null == accountManager.getPassword(newAccount) ) { /* * 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)) { return null; } /* * If you don't set android:syncable="true" in * in your
element in the manifest, * then call ContentResolver.setIsSyncable(account, AUTHORITY, 1) * here. */ } return newAccount; }
8. 创建SyncService服务
该服务用于将SyncAdapter开放给framework调用,即将SyncAdapter的binder对象传给framework。通过这个binder,framework即可调用onPerformSync()
方法。
在onCreate()
中以单实例形式实例化SyncAdapter,这样会将SyncAdapter的实例化延迟到framework首次传输数据创建Service的时候执行。实例化过程须保证线程安全,避免将多次同步响应添加到队列。
示例代码:
/**
* 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();
}
}
9. 添加Account
framework要求每个SyncAdapter必须有一个账户类型,对应第三步中Authenticator的元数据文件,需要在Android系统中设置账户类型:调用addAccountExplicitly()
方法,天价一个具有账户类型的虚拟账户。最好是在打开应用时的onCreate()
中调用此方法。以下是Android提供的示例代码:
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 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.
*/
}
}
...
}
10. 添加SyncAdapter的元数据文件
元数据制订了SyncAdapter的账户类型、对应ContentProvider的Authority、系统和SyncAdapter相关的部分UI以及其他一些同步相关的标识。文件名一般取syncadapter.xml
,保存在/res/xml/
目录下,根标签为
。
11. 在清单中声明SyncAdapter
需要添加四个权限,并声明SyncService:
...
...
设置了一个由Action为android.content.SyncAdapter的Intent触发的过滤器,系统要运行SyncAdapter时会发送这个Intent。当过滤器被触发时,系统会创建绑定用的Service,本例中即SyncService。
android:exported="true"
允许出此应用之外的进程来访问这个Service。
android:process=":sync"
告诉系统在名为sync 的全局共享的进程中运行这个Service。如果你的应用中有多个SyncAdapter,他们可以共享这个进程,可以降低一些消耗。
元素规定了之前创建SyncAdapter元数据XML文件。android:name
属性说明这个元数据是给同步框架的。android:resource
元素指定了元数据文件的名字。
如何运行SyncAdapter?
- 可以用以下几种方式运行SyncAdapter:
- 服务端数据变化时
- 本地数据变化时
- 系统发送网络消息时
- 固定时间间隔或时间点
- 手动发起
服务端数据变更时同步
当服务端数据发生变更,服务端发送一条特殊的消息到应用的BroadcastReceiver,然后调用ContentResolver.requestSync()
发起同步。Google Cloud Messaging提供了发送此消息的服务端和客户端组件,使用GCM比轮询服务器更可靠更有效率。以下是示例代码:
public class GcmBroadcastReceiver extends BroadcastReceiver {
...
// 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注册一个Observer,当数据变化后,framework会调用这个Observer,在Observer中调用requestSync()
开启同步。
要给ContentProvider创建Observer,只需继承ContentObserver并实现其中的onChange()
方法,在onChange()
方法中调用requestSync()
运行SyncAdapter。
注册Observer,将其作为参数传入registerContentObserver()
,还需传入需要监听的URI。ContentProvider框架会将此URI与传入的URI进行比较,匹配成功则调用ContentProvider.onChange()
方法。
示例代码:
public class MainActivity extends FragmentActivity {
...
// 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);
...
}
...
}
网络消息触发后同步
当网络连接可用时,Android系统会每隔几秒钟发送一条消息来保持手机的TCP/IP连接打开。这个消息也会到达每个应用的ContentResolver。
通过调用setSyncAutomatically()
,可以设置ContentResolve在收到消息时自动发起同步。
通过设置在收到网络消息时发起同步,能确保在网络可用时发起同步。 如果你不需要在数据变化时立即强制发起同步,但是又想要保证数据有规律的更新,则可以使用这个选项。类似的,如果你不希望按照固定的时间间隔来同步但又希望能够频繁点的同步的话,也可以使用这个选项。
由于setSyncAutomatically()
不会禁用addPeriodicSync()
,因此SyncAdapter可能会被频繁的重复触发。因此如果要定期的触发同步,就应该禁用掉setSyncAutomatically()
。
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);
...
}
...
}
定时同步
设置固定的时间点或时间间隔进行同步。
调用addPeriodicSync()
设置固定的时间间隔。
如果调用setInexactRepeating()
设置触发时间,那么时间应设为随机,保证不同设备的同步时间错开。
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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
SyncRequest request = new SyncRequest.Builder()
.syncPeriodic(SYNC_INTERVAL, FLEX_TIME)
.setSyncAdapter(ACCOUNT, AUTHORITY)
.setExtras(Bundle.EMPTY).build();
ContentResolver.requestSync(request);
} else {
ContentResolver.addPeriodicSync(
ACCOUNT,
AUTHORITY,
Bundle.EMPTY,
SYNC_INTERVAL);
}
ContentResolver.setSyncAutomatically(ACCOUNT, AUTHORITY, true);
...
}
...
}
手动请求同步
这是最不建议的同步策略,浪费了SyncAdapter同步框架的优点。如果还是需要手动发起同步,那么设置手动同步的flag,然后调用ContentResolver.requestSync()
。需要使用以下flag:
SYNC_EXTRAS_MANUAL
强制发起手动同步,同步框架会忽略当前的一些设置,比如自动同步开关状态。
SYNC_EXTRAS_EXPEDITED
强制立即发起同步。如果不设置这个选项,系统为了优化功耗可能会等待几秒钟,将一段时间内的几次同步合并发起。
此步骤与SyncAdapter中的syncImmediately()
方法功能相同,以下是Android提供的示例代码:
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,则不能发起自动同步。