SyncAdapter同步机制

版权声明:本文为博主原创文章,未经博主允许不得转载。技术交流可邮:[email protected] https://blog.csdn.net/cjh94520/article/details/74066404

官方文档:https://developer.android.com/training/sync-adapters/index.html

1.简介

在Android设备和web服务器之间同步数据会使你的应用更实用,更吸引用户,例如,将手机数据传到服务端实现数据备份,将数据从服务端取回让用户能够脱机使用。在某些情况下,用户会发现这样会更方便:通过web修改信息然后在手机上就可以继续使用,或者隔一段时间将手机上的数据上传到一个总存储区。
虽然你可以在应用中设计自己的数据传输系统,但也应该考虑一下用Android的sync adater框架。它可以协助管理和自动发起数据传输,也可以协调不同应用的同步操作。使用这个同步框架比自己设计数据传输策略有如下优势:
- 插件架构:
可以将数据传输的代码以可调用组件的形式添加到系统中。
- 自动执行:
可以根据不同条件自动发起数据传输,比如数据变更,间隔一定时间,或者是每天定时。而且,系统会将暂时不能运行的操作添加到队列里,在可能的情况下重新发起。
- 自动检查网络:
系统只会在有网络的情况下发起数据传输
- 优化电池性能:
可以集中处理数据传输任务。将你的应用的数据传输与其他应用的传输结合,减少系统切换网络的次数,从而降低功耗。
- 账号管理认证:
如果你的应用需要用户认证功能,你可以选择在数据传输中整合进账号管理认证。

1.1能用来做什么

  • Facebook可以定期更新朋友的最新信息,将最新近况和心情短语集成入联系人中。
  • 笔记应用的云备份
  • 账户信息的同步

2.同步框架结构

这里写图片描述

账号同步框架组成部分有三,如上。三者缺一不可。账号是入口,里面可以进行账号验证操作,当然不需要这个功能,相应方法返回false或者null即可。通过了账号认证之后,到了同步管理,里面来进行数据的同步的操作,至于数据发生冲突的具体逻辑需要你来处理。还有个StubProvider,是用来配合同步更新操作的。

3.账号管理

3.1AuthenticationService类

  • AuthenticationService是一个继承Service的服务,目的是提供跨进程调用,供系统同步框架调用。
  • 固定的Action为android.accounts.AccountAuthenticator

下面是manifest中的注册:

    <service
        android:name=".AuthenticatorService"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.accounts.AccountAuthenticator" />
        </intent-filter>

        <meta-data
            android:name="android.accounts.AccountAuthenticator"
            android:resource="@xml/authenticator" />
    </service>

对应的服务:

public class AuthenticatorService extends Service {
    //mAuthenticator目的是作为账号认证
    private Authenticator mAuthenticator;
    public AuthenticatorService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mAuthenticator = new Authenticator(this);
    }

    @Override
    public IBinder onBind(Intent intent) {
        //主要起作用的mAuthenticator
        return mAuthenticator.getIBinder();
    }
}

3.2Authenticator类

Authenticator是一个继承自AbstractAccountAuthenticator的类,AbstractAccountAuthenticator是一个虚类,它定义处理手机“设置”里“账号与同步”中Account的添加、删除和验证等功能的基本接口,并实现了一些基本功能。

AbstractAccountAuthenticator里面有个继承于IAccountAuthenticator.Stub的内部类,以用来对AbstractAccountAuthenticator的远程接口调用进行包装。我们可以通过AbstractAccountAuthenticator的getIBinder()方法,返回内部类的IBinder形式,以便对此类进行远程调用,如上面代码onBind方法中的调用。

其中比较重要需要重载的方法是addAccount():

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle) throws NetworkErrorException {
        Log.d(TAG, "Authenticator addAccount  :  ");

        Intent intent = new Intent("com.jiahui.xx.syncadapter");

        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, accountAuthenticatorResponse);

        bundle.putParcelable(AccountManager.KEY_INTENT, intent);

        return bundle;
        //return null;
    }

如上图,这个addAccount()在用户进入设置-账户-添加账户的时候触发的,这里面把自己设置账户的页面的信息封装给bundle,然后传出去即可。如果返回null表示不做任何触发。

3.3authenticator.xml

<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.crazyman.accountsyncdemo.type"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:smallIcon="@mipmap/ic_launcher" />

4.同步管理

sync机制的使用和账号管理很类似,也是基于binder机制的跨进程通信。首先它需要一个Service,这个服务提供一个Action给系统以便系统能找到它;然后就是继承和实现AbstractThreadedSyncAdapter,此类中包含实现了ISyncAdapter.Stub内部类,这个内部类封装了远程接口调用,这个类getSyncAdapterBinder()方法,返回内部类的IBinder形式,以便对AbstractThreadedSyncAdapte进行远程调用;在manifest中需要对Service注册,而且指定meta-data,这个meta-data是一个xml文件,在SampleSyncAdapter实例中,它的名字是syncadapter.xml,这个文件指定了账号和被监听的contentprovider。下面分别介绍这几个文件:

4.1SyncService

public class SyncService extends Service {
    private static final String TAG = "SyncService";

    private static final Object sSyncAdapterLock = new Object();
    private static SyncAdapter sSyncAdapter = null;

    /**
     * Thread-safe constructor, creates static {@link SyncAdapter} instance.
     */
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "Service created");
        synchronized (sSyncAdapterLock) {
            if (sSyncAdapter == null) {
                sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
            }
        }
    }

    @Override
    /**
     * Logging-only destructor.
     */
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "Service destroyed");
    }

    /**
     * Return Binder handle for IPC communication with {@link SyncAdapter}.
     *
     * <p>New sync requests will be sent directly to the SyncAdapter using this channel.
     *
     * @param intent Calling intent
     * @return Binder handle for {@link SyncAdapter}
     */
    @Override
    public IBinder onBind(Intent intent) {
        return sSyncAdapter.getSyncAdapterBinder();
    }
}
  • SyncService是一个继承普通Service的服务,用来给远端进程提供服务,在onBind方法中返回IBinder。

Manifest.xml注册如下:

<service
            android:name=".syncadapter.SyncService"
            android:exported="true">
            <intent-filter>
                <action
                    android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/syncadapter" />

        </service>
  • 一个适配器只能同步一个Authority,若想使一个账户同步多个Authority,可以向系统注册多个绑定同一账户的sync-adapter。

4.2syncadapter.xml

<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.crazyman.accountsyncdemo.type"
    android:allowParallelSyncs="false"
    android:contentAuthority="com.crazyman.accountsyncdemo.provider"
    android:isAlwaysSyncable="true"
    android:supportsUploading="false"
    android:userVisible="false" />

syncadapter.xml文件指定了此Service所监听的contentprovider的Authority,还指定了监听此Authority的账号类型accountType。常用属性以及作用如下:

属性 描述
android:contentAuthority 指定要同步的ContentProvider所需的权限
android:accountType 表示进行同步的账号的类型
android:userVisible 设置是否在“设置–账户”中显示,当设置为true时,用户可手动关闭同步功能
android:supportsUploading 设置是否必须notifyChange通知才能同步
android:allowParallelSyncs 是否支持并发同步
android:isAlwaysSyncable 默认是false,framework是否可以在任意时刻运行SyncAdapter,如果仅希望通过程序控制同步发起,则设为false,然后通过调用requestSync()发起

4.3SyncAdapter.java

public class SyncAdapter extends AbstractThreadedSyncAdapter {
    private static final String TAG = SyncAdapter.class.getSimpleName();

    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
        Log.d(TAG, "SyncAdapter onPerformSync  :  同步数据开始");
        // TODO: 2017/7/1 执行具体数据同步工作
        Log.d(TAG, "SyncAdapter onPerformSync  :  同步数据结束");
    }
}
  • AbstractThreadedSyncAdapter内部提供startSync()和cancelSync()两个方法,两个方法主要是被远端系统进程调用。startSync()将会启动一个线程,通过在该线程中调用AbstractThreadedSyncAdapter的 onPerformSync()方法来执行同步操作。所以上面onPerformSync方法中的操作都是在新线程中执行的。
  • cancelSync()将会中断同步操作。

5.StubProvider

前面说了,整个框架必须有个ContentProvider作为组成部分,当然你实际也可以不使用,在syncadapter的:

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
        Log.d(TAG, "SyncAdapter onPerformSync  :  同步数据开始");
        // TODO: 2017/7/1 执行具体数据同步工作
        Log.d(TAG, "SyncAdapter onPerformSync  :  name:  " + account.name + "     type:   " + account.type);
        Log.d(TAG, "SyncAdapter onPerformSync  :  " + authority);
        Log.d(TAG, "SyncAdapter onPerformSync  :  同步数据结束");
    }

里面传递过来的参数provider就是配置的Contentprivder。

而且这个StubProvider需要在Manifest.xml中注册,还有需要设置属性syncable = true:

        <provider
            android:name="com.crazyman.accountsyncdemo.StubProvider"
            android:authorities="com.crazyman.accountsyncdemo.provider"
            android:exported="false"
            android:syncable="true" />

如果不在Manifest.xml中设置,也可以在代码中设置

 ContentResolver.setIsSyncable(account, "com.crazyman.accountsyncdemo.provider", 1);

两者一致,最后一个参数的意思是:

@param syncable >0 denotes syncable, 0 means not syncable, <0 means unknowns

6.同步机制运作流程

同步框架有几种数据同步的情况需要处理:

1. 数据在服务端变化,-看6.1;
2. 数据在客户端变化,-看6.2;
3. 系统连接TCL长连接,-看6.3;
4. 设置周期发送同步,-看6.4
5. 手动强制同步,-看6.5

6.1服务端如何同步到客户端

例如像Facebook这种项目,除了移动端app,还有web版,假设在web版数据变化了,如何通知到移动端呢?首先这个数据的检查需要自己动手做,Google原生提供了GCM的机制,可以发送notify到移动端,我们只需要在移动端进行监听对应的广播就可以了:
1.判断消息类型,判断是否需要同步
2.调用ContentResolver.requestSync();

 @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);
            ...
        }
        ...
    }

ContentResolver.requestSync()这个方法最后会调用到ContentService.syncAsUser(),然后调用到SyncManager.scheduleSync()进行同步

6.2客户端如何同步数据到服务端

当URI对应的数据变化时如何通知:

1. 在配置syncadapter.xml的时候,配了一个ContentProvider的权限;
2. 当该权限对应的ContentProvider的数据变化时候,在客户端处调用ContentResolver.notifyChange(Android.net.Uri,android.database.ContentObserver, boolean)这个方法来通知我们;
3. ContentResolver.notifyChange->getContentService().notifyChange()->ContentService.notifyChange();

下面观察ContentService.notifyChange()的代码如下:

    @Override
    public void notifyChange(Uri uri, IContentObserver observer,
                             boolean observerWantsSelfNotifications, int flags,
                             int userHandle) {
        try {
            ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
            synchronized (mRootNode) {
                mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications,
                        flags, userHandle, calls);
            }
            final int numCalls = calls.size();
            for (int i=0; i<numCalls; i++) {
                ObserverCall oc = calls.get(i);
                try {
                    oc.mObserver.onChange(oc.mSelfChange, uri, userHandle);
                    if (DEBUG) Slog.d(TAG, "Notified " + oc.mObserver + " of " + "update at "
                            + uri);
                } catch (RemoteException ex) {
                    synchronized (mRootNode) {
                        Log.w(TAG, "Found dead observer, removing");
                        IBinder binder = oc.mObserver.asBinder();
                        final ArrayList<ObserverNode.ObserverEntry> list
                                = oc.mNode.mObservers;
                        int numList = list.size();
                        for (int j=0; j<numList; j++) {
                            ObserverNode.ObserverEntry oe = list.get(j);
                            if (oe.observer.asBinder() == binder) {
                                list.remove(j);
                                j--;
                                numList--;
                            }
                        }
                    }
                }
            }
            if ((flags& ContentResolver.NOTIFY_SYNC_TO_NETWORK) != 0) {
                SyncManager syncManager = getSyncManager();
                if (syncManager != null) {
                    syncManager.scheduleLocalSync(null /* all accounts */, callingUserHandle, uid,
                            uri.getAuthority());
                }
            }

            synchronized (mCache) {
                final String providerPackageName = getProviderPackageName(uri);
                invalidateCacheLocked(userHandle, providerPackageName, uri);
            }
        } finally {
            restoreCallingIdentity(identityToken);
        }
    }

上面一共做了2个事情:
1. 对该ContentProvider的所有感兴趣的Observer进行通知;
2. 对SyncManager注册的对该URI感兴趣的syncadapter进行通知。

6.3系统连接TCL长连接

  ContentResolver.setSyncAutomatically(account, "com.crazyman.accountsyncdemo.provider", true);

开启这个选项之后,会在网络连接的状态下进行自动同步

6.4设置周期发送同步

ContentResolver.addPeriodicSync(account, "com.crazyman.accountsyncdemo.provider", Bundle.EMPTY, 60 * 3);

固定间隔一个时间进行自动同步,如上方法,最后一个参数代表时间,单位是秒

6.5客户端强制发起同步

 Bundle settingsBundle = new Bundle();
        settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);

flag:
SYNC_EXTRAS_MANUAL
强制发起手动同步,同步框架会忽略当前的一些设置,比如自动同步开关状态。
SYNC_EXTRAS_EXPEDITED
强制立即发起同步。如果不设置这个选项,系统为了优化功耗可能会等待几秒钟,将一段时间内的几次同步合并发起。

7.权限设置

一般需要下面几个权限

    <!-- 一般同步数据需要连接网络 -->
    <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" />

8.常见问题

8.1同步框架发起条件

手动同步和自动同步需要条件:
Provider的isSyncable = true
SyncAdapter的isAlwaysSyncable = trye

自动同步还需要:
系统总同步开关( getMasterSyncAutomatically = true)
SyncAdapter同步开关( getSyncAutomatically = true)

ContentResolver 的notifyChange()发起自动同步时会带SYNC_EXTRAS_UPLOAD标志,Android设计原意是仅将本地数据更新至服务端。此时SyncAdapter中的supportsUploading若是false,则不能发起自动同步。

8.2设置-账户可见问题

当文件adapter.xml设置属性 android:userVisible=”true” ,同步设置选项会对用户可见,用户可以选择手动关闭自动同步功能。

8.3账户添加-空白页面停留问题

在设置-账户-添加账户,会触发对应应用的Authenticator类的addAccount()方法,addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle);
一般情况下在这个方法的bundle带上跳转到另外的账户设置页面intent进行账户添加,当然也可以在这个方法体里面直接做添加账户的操作,然后返回null。这种情况下处理完添加账户的逻辑,该页面不会退出,所以需要调用对应的成功设置方法。

        Account account = new Account("联系人", "com.crazyman.accountsyncdemo.type");
        AccountManager accountManager = (AccountManager) mContext.getSystemService(ACCOUNT_SERVICE);
        boolean isCreated = accountManager.addAccountExplicitly(account, null, null);

        //accountAuthenticatorResponse.onResult防止停留在空白画面
        if (isCreated) {
            Bundle result = new Bundle();
            result.putString(AccountManager.KEY_ACCOUNT_NAME, "联系人");
            result.putString(AccountManager.KEY_ACCOUNT_TYPE, "com.crazyman.accountsyncdemo.type");
            accountAuthenticatorResponse.onResult(result);
        }
        return null;

参考来自:

http://blog.csdn.net/inconsolabl/article/details/48054341
http://www.jianshu.com/p/dc9a2693478e
http://www.2cto.com/kf/201411/352718.html
http://www.cnblogs.com/fengzhblog/p/3177002.html

猜你喜欢

转载自blog.csdn.net/cjh94520/article/details/74066404
今日推荐