您的位置:首页 > 移动开发 > Android开发

Android SyncAdapter同步实践

2016-07-26 19:33 375 查看
官方文档:https://developer.android.com/training/sync-adapters/creating-sync-adapter.html

中文翻译文档:http://hukai.me/android-training-course-in-chinese/connectivity/sync-adapters/create-sync-adapter.html

1. 创建Sync Adapter

1.1 创建SyncAdapter子类,继承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();
...
}


1.2 在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.
*/
...
}


我们可以使用SyncResult返回同步结果给SyncManager同步框架。根据导致同步失败的原因,区分hard error(如:鉴权失败、数据库操作失败、重试次数过多等)和soft error(如:正在同步中、网络错误等)。其中soft error情况下同步框架会同步重试,但hard error情况下同步框架不再同步重试。

/**
* This class is used to communicate the results of a sync operation to the SyncManager.
* Based on the values here the SyncManager will determine the disposition of the
* sync and whether or not a new sync operation needs to be scheduled in the future.
*
*/
public final class SyncResult implements Parcelable {
/**
* Used to indicate that the SyncAdapter is already performing a sync operation, though
* not necessarily for the requested account and authority and that it wasn't able to
* process this request. The SyncManager will reschedule the request to run later.
*/
public final boolean syncAlreadyInProgress;

/**
* Used to indicate that the SyncAdapter determined that it would need to issue
* too many delete operations to the server in order to satisfy the request
* (as defined by the SyncAdapter). The SyncManager will record
* that the sync request failed and will cause a System Notification to be created
* asking the user what they want to do about this. It will give the user a chance to
* choose between (1) go ahead even with those deletes, (2) revert the deletes,
* or (3) take no action. If the user decides (1) or (2) the SyncManager will issue another
* sync request with either {@link ContentResolver#SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS}
* or {@link ContentResolver#SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS} set in the extras.
* It is then up to the SyncAdapter to decide how to honor that request.
*/
public boolean tooManyDeletions;

/**
* Used to indicate that the SyncAdapter experienced a hard error due to trying the same
* operation too many times (as defined by the SyncAdapter). The SyncManager will record
* that the sync request failed and it will not reschedule the request.
*/
public boolean tooManyRetries;

/**
* Used to indicate that the SyncAdapter experienced a hard error due to an error it
* received from interacting with the storage layer. The SyncManager will record that
* the sync request failed and it will not reschedule the request.
*/
public boolean databaseError;

/**
* If set the SyncManager will request an immediate sync with the same Account and authority
* (but empty extras Bundle) as was used in the sync request.
*/
public boolean fullSyncRequested;

/**
* This field is ignored by the SyncManager.
*/
public boolean partialSyncUnavailable;

/**
* This field is ignored by the SyncManager.
*/
public boolean moreRecordsToGet;

/**
* Used to indicate to the SyncManager that future sync requests that match the request's
* Account and authority should be delayed at least this many seconds.
*/
public long delayUntil;

/**
* Used to hold extras statistics about the sync operation. Some of these indicate that
* the sync request resulted in a hard or soft error, others are for purely informational
* purposes.
*/
public final SyncStats stats;

/**
* This instance of a SyncResult is returned by the SyncAdapter in response to a
* sync request when a sync is already underway. The SyncManager will reschedule the
* sync request to try again later.
*/
public static final SyncResult ALREADY_IN_PROGRESS;

static {
ALREADY_IN_PROGRESS = new SyncResult(true);
}
...
/**
* Convenience method for determining if the SyncResult indicates that a hard error
* occurred. See {@link #SyncResult()} for an explanation of what the SyncManager does
* when it sees a hard error.
* <p>
* A hard error is indicated when any of the following is true:
* <ul>
* <li> {@link SyncStats#numParseExceptions} > 0
* <li> {@link SyncStats#numConflictDetectedExceptions} > 0
* <li> {@link SyncStats#numAuthExceptions} > 0
* <li> {@link #tooManyDeletions}
* <li> {@link #tooManyRetries}
* <li> {@link #databaseError}
* @return true if a hard error is indicated
*/
public boolean hasHardError() {
return stats.numParseExceptions > 0
|| stats.numConflictDetectedExceptions > 0
|| stats.numAuthExceptions > 0
|| tooManyDeletions
|| tooManyRetries
|| databaseError;
}

/**
* Convenience method for determining if the SyncResult indicates that a soft error
* occurred. See {@link #SyncResult()} for an explanation of what the SyncManager does
* when it sees a soft error.
* <p>
* A soft error is indicated when any of the following is true:
* <ul>
* <li> {@link SyncStats#numIoExceptions} > 0
* <li> {@link #syncAlreadyInProgress}
* </ul>
* @return true if a soft error is indicated
*/
public boolean hasSoftError() {
return syncAlreadyInProgress || stats.numIoExceptions > 0;
}

/**
* A convenience method for determining of the SyncResult indicates that an error occurred.
* @return true if either a soft or hard error occurred
*/
public boolean hasError() {
return hasSoftError() || hasHardError();
}
...
}


2. 将SyncAdapter绑定到SyncManager同步框架

现在,我们已经将数据传输代码封装在 Sync Adapter 组件中,但是我们必须让框架可以访问我们的代码。为了做到这一点,我们需要创建一个绑定Service,它将一个特殊的 Android Binder 对象从 Sync Adapter 组件传递给框架。有了这一Binder对象,框架就可以调用onPerformSync()方法并将数据传递给它。Sync Adapter 组件实例化为一个单例,以防止框架在响应触发和调度时,形成含有多个 Sync Adapter 执行的队列。

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();
}
}


3. 添加Sync Adapter的元数据文件

要将我们的 Sync Adapter 组件集成到框架中,我们需要向框架提供描述组件的元数据,以及额外的标识信息。元数据指定了我们为 Sync Adapter 所创建的账户类型,声明了一个和应用相关联的 Content Provider Authority,对和 Sync Adapter 相关的一部分系统用户接口进行控制,同时还声明了其它同步相关的标识。在我们项目的/res/xml目录下的一个特定文件内声明这一元数据,我们可以为这个文件命名,不过通常来说我们将其命名为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"/>


其中属性的含义如下:

<declare-styleable name="SyncAdapter">
<!-- the authority of a content provider. -->
<attr name="contentAuthority" format="string"/>
<attr name="accountType"/>
<attr name="userVisible" format="boolean"/>
<attr name="supportsUploading" format="boolean"/>
<!-- Set to true to tell the SyncManager that this SyncAdapter supports
multiple simultaneous syncs for the same account type and authority.
Otherwise the SyncManager will be sure not to issue a start sync request
to this SyncAdapter if the SyncAdapter is already syncing another account.
Defaults to false.
-->
<attr name="allowParallelSyncs" format="boolean"/>
<!-- Set to true to tell the SyncManager to automatically call setIsSyncable(..., ..., 1)
for the SyncAdapter instead of issuaing an initialization sync to the SyncAdapter.
Defaults to false.
-->
<attr name="isAlwaysSyncable" format="boolean"/>
<!-- If provided, specifies the action of the settings
activity for this SyncAdapter.
-->
<attr name="settingsActivity"/>
</declare-styleable>


4. 添加框架所需的账户

Sync Adapter 框架需要每个 Sync Adapter 拥有一个账户类型。在创建 Stub 授权器章节中,我们已经声明了账户类型的值。现在我们需要在 Android 系统中配置该账户类型。要配置账户类型,通过调用addAccountExplicitly()添加一个使用其账户类型的虚拟账户。

5. 在Manifest清单文件中声明Sync Adapter

5.1 相关权限申明

android.permission.INTERNET

允许访问网络,使得它可以从设备下载和上传数据到服务器。如果之前已经请求了该权限,那么就不需要重复请求了。

android.permission.READ_SYNC_SETTINGS

允许应用读取当前的 Sync Adapter 配置。例如,我们需要该权限来调用getIsSyncable()。

android.permission.WRITE_SYNC_SETTINGS

允许我们的应用 对Sync Adapter 的配置进行控制。我们需要这一权限来通过addPeriodicSync()方法设置执行同步的时间间隔。另外,调用requestSync()方法不需要用到该权限。

<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"/>
...
</manifest>


5.2 声明框架用来和 Sync Adapter 进行交互的绑定Service

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


问题1:数据量较大时容易失败,内容错乱

分析

log中发现同步线程穿插执行

解决方案

同一时刻保证只有一个同步线程执行。在SyncAdapter和同步框架建立连接后,同步框架将通过binder(AbstractThreadedSyncAdapter中的ISyncAdapter)调用同步startSync,应用首先会创建一个同步线程SyncThread,然后这个线程会回调onPerformSync。

public abstract class AbstractThreadedSyncAdapter {
/**
* Kernel event log tag.  Also listed in data/etc/event-log-tags.
* @deprecated Private constant.  May go away in the next release.
*/
@Deprecated
public static final int LOG_SYNC_DETAILS = 2743;

private final Context mContext;
private final AtomicInteger mNumSyncStarts;
private final ISyncAdapterImpl mISyncAdapterImpl;
...
private Account toSyncKey(Account account) {
if (mAllowParallelSyncs) {
return account;
} else {
return null;
}
}
private class ISyncAdapterImpl extends ISyncAdapter.Stub {
@Override
public void startSync(ISyncContext syncContext, String authority, Account account,
Bundle extras) {
final SyncContext syncContextClient = new SyncContext(syncContext);

boolean alreadyInProgress;
// synchronize to make sure that mSyncThreads doesn't change between when we
// check it and when we use it
final Account threadsKey = toSyncKey(account);
synchronized (mSyncThreadLock) {
if (!mSyncThreads.containsKey(threadsKey)) {
if (mAutoInitialize
&& extras != null
&& extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) {
try {
if (ContentResolver.getIsSyncable(account, authority) < 0) {
ContentResolver.setIsSyncable(account, authority, 1);
}
} finally {
syncContextClient.onFinished(new SyncResult());
}
return;
}
SyncThread syncThread = new SyncThread(
"SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(),
syncContextClient, authority, account, extras);
mSyncThreads.put(threadsKey, syncThread);
syncThread.start();
alreadyInProgress = false;
} else {
alreadyInProgress = true;
}
}

// do this outside since we don't want to call back into the syncContext while
// holding the synchronization lock
if (alreadyInProgress) {
syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS);
}
}
...
}


从上面的源码中,可以发现当设置了mAllowParallelSyncs这个标志后,同一个账号可以保证同一时刻只创建一个同步线程。那剩下的事情就是评估设置这个标志位后会不会有什么副作用呢?在添加Sync Adapter的元数据文件时,属性allowParallelSyncs,官方注释:

<!-- Set to true to tell the SyncManager that this SyncAdapter supports
multiple simultaneous syncs for the same account type and authority.
Otherwise the SyncManager will be sure not to issue a start sync request
to this SyncAdapter if the SyncAdapter is already syncing another account.
Defaults to false.
-->
<attr name="allowParallelSyncs" format="boolean"/>


初步看起来标志mAllowParallelSyncs跟属性allowParallelSyncs似乎很有关联了,但事实上属性allowParallelSyncs只会影响同步框架调度同步任务,而mAllowParallelSyncs的作用范围局限在AbstractThreadedSyncAdapter中。所以决定通过设置mAllowParallelSyncs,以最小的修改来达到我们的目的。

注意:需要将SyncAdapter组件设计成单例模式,这也是官方文档推荐的。否则,如果产生不同的AbstractThreadedSyncAdapter,达不到保证同一时刻只创建一个同步线程的目的。

最后修改如下,SyncAdapter组件单例设计,并使用AbstractThreadedSyncAdapter的支持allowParallelSync参数的构造函数:

public class NoteSyncAdapterService extends Service{
static SyncAdapterImpl sSyncAdapter = null;
//private final Object sSyncAdapterLock = new Object();
@Override
public void onCreate() {
super.onCreate();
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapterImpl(this);
}
}
...
class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
public SyncAdapterImpl(Context context) {
super(context, true, true);
}
...
}
}


问题2:取消同步后同步线程没有退出

分析

同步框架取消同步时会调用Sync Adapter的cancelSync执行syncThread.interrupt设置中断标志位,在同步的过程中通过Thread.isInterrupt不断检查中断标志来决定是否需要退出线程,但很多情况下中断标志都会被清除,除了已知的If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException. 主动调用Thread.interrupted,在Android平台上调试发现数据库操作也会清除中断标志,而同步过程中有大量的数据库操作。

解决方案

自定义全局标志位

public volatile static boolean syncCancelFlag = false;

检查全局标志位,抛异常退出同步线程

private void checkCancelled() throws SyncCancelException {
if(syncCancelFlag){
throw new SyncCancelException();
}
}


由于Sync Adapter设置了mAllowParallelSyncs,同步框架取消同步时将回调onSyncCanceled(Thread thread), 在这里我们设置全局标志位

public class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
...
@Override
public void onSyncCanceled(Thread thread) {
syncCancelFlag = true;
super.onSyncCanceled(thread);
}
...
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android