创建同步适配器

注意: 我们建议使用 WorkManager 作为大多数后台处理用例的推荐解决方案。请参考 后台处理指南 了解哪种解决方案最适合您。

应用中的同步适配器组件封装了在设备和服务器之间传输数据的任务的代码。根据您在应用中提供的计划和触发器,同步适配器框架会在同步适配器组件中运行代码。若要将同步适配器组件添加到您的应用,您需要添加以下部分

同步适配器类。
一个将数据传输代码包装在与同步适配器框架兼容的接口中的类。
绑定 Service
允许同步适配器框架运行同步适配器类中的代码的组件。
同步适配器 XML 元数据文件。
包含有关同步适配器的信息的文件。框架读取此文件以了解如何加载和计划数据传输。
应用清单中的声明。
声明绑定服务并指向同步适配器特定元数据的 XML。

本课程将向您展示如何定义这些元素。

创建同步适配器类

在本课程的这一部分中,您将学习如何创建封装数据传输代码的同步适配器类。创建类包括扩展同步适配器基类、为类定义构造函数以及实现定义数据传输任务的方法。

扩展基同步适配器类

要创建同步适配器组件,请先扩展 AbstractThreadedSyncAdapter 并编写其构造函数。使用构造函数在每次从头开始创建同步适配器组件时运行设置任务,就像您使用 Activity.onCreate() 设置 Activity 一样。例如,如果您的应用使用内容提供程序存储数据,请使用构造函数获取 ContentResolver 实例。由于在 Android 平台版本 3.0 中添加了构造函数的第二种形式以支持 parallelSyncs 参数,因此您需要创建构造函数的两种形式以保持兼容性。

注意: 同步适配器框架旨在与作为单例实例的同步适配器组件配合使用。在 将同步适配器绑定到框架 部分中将更详细地介绍同步适配器组件的实例化。

以下示例演示了如何实现 AbstractThreadedSyncAdapter 及其构造函数

Kotlin

/**
 * Handle the transfer of data between a server and an
 * app, using the Android sync adapter framework.
 */
class SyncAdapter @JvmOverloads constructor(
        context: Context,
        autoInitialize: Boolean,
        /**
         * Using a default argument along with @JvmOverloads
         * generates constructor for both method signatures to maintain compatibility
         * with Android 3.0 and later platform versions
         */
        allowParallelSyncs: Boolean = false,
        /*
         * If your app uses a content resolver, get an instance of it
         * from the incoming Context
         */
        val mContentResolver: ContentResolver = context.contentResolver
) : AbstractThreadedSyncAdapter(context, autoInitialize, allowParallelSyncs) {
    ...
}

Java

/**
 * 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 contentResolver;
    /**
     * 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
         */
        contentResolver = 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
         */
        contentResolver = context.getContentResolver();
        ...
    }

添加数据传输代码

同步适配器组件不会自动执行数据传输。相反,它封装了您的数据传输代码,以便同步适配器框架可以在后台运行数据传输,而无需您的应用参与。当框架准备好同步应用的数据时,它会调用您对 onPerformSync() 方法的实现。

为了便于将数据从主应用代码传输到同步适配器组件,同步适配器框架会使用以下参数调用 onPerformSync()

帐户
与触发同步适配器的事件关联的 Account 对象。如果您的服务器不使用帐户,则无需使用此对象中的信息。
额外信息
包含触发同步适配器的事件发送的标志的 Bundle
权限
系统中内容提供程序的权限。您的应用必须能够访问此提供程序。通常,权限对应于您自己的应用中的内容提供程序。
内容提供程序客户端
权限参数指向的内容提供程序的 ContentProviderClientContentProviderClient 是内容提供程序的轻量级公共接口。它具有与 ContentResolver 相同的基本功能。如果您使用内容提供程序来存储应用的数据,则可以使用此对象连接到该提供程序。否则,您可以忽略它。
同步结果
您用于向同步适配器框架发送信息的 SyncResult 对象。

以下代码片段展示了onPerformSync()的整体结构。

Kotlin

/*
 * 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 fun onPerformSync(
        account: Account,
        extras: Bundle,
        authority: String,
        provider: ContentProviderClient,
        syncResult: SyncResult
) {
    /*
     * Put the data transfer code here.
     */
}

Java

/*
 * 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()的实际实现取决于您应用的数据同步需求和服务器连接协议,但您的实现应该执行一些常规任务。

连接到服务器
虽然您可以在数据传输开始时假设网络可用,但同步适配器框架不会自动连接到服务器。
下载和上传数据
同步适配器不会自动执行任何数据传输任务。如果您想从服务器下载数据并将其存储在内容提供程序中,则必须提供请求数据、下载数据并将其插入提供程序的代码。同样,如果您想将数据发送到服务器,则必须从文件、数据库或提供程序中读取数据,并发送必要的上传请求。您还必须处理数据传输过程中发生的网络错误。
处理数据冲突或确定数据的最新程度
同步适配器不会自动处理服务器上的数据与设备上的数据之间的冲突。此外,它不会自动检测服务器上的数据是否比设备上的数据更新,反之亦然。相反,您必须提供自己的算法来处理这种情况。
清理。
在数据传输结束时,始终关闭与服务器的连接并清理临时文件和缓存。

注意:同步适配器框架在后台线程上运行onPerformSync(),因此您无需设置自己的后台处理。

除了与同步相关的任务外,您还应尝试将常规的网络相关任务组合起来,并将其添加到onPerformSync()中。通过将所有网络任务集中在此方法中,您可以节省启动和停止网络接口所需的电池电量。要详细了解如何提高网络访问效率,请参阅培训课程在不耗尽电池电量的情况下传输数据,其中介绍了可以在数据传输代码中包含的几个网络访问任务。

将同步适配器绑定到框架

现在,您已将数据传输代码封装在同步适配器组件中,但您必须为框架提供对代码的访问权限。为此,您需要创建一个绑定Service,该服务将来自同步适配器组件的特殊 Android 绑定器对象传递到框架。使用此绑定器对象,框架可以调用onPerformSync()方法并将数据传递给它。

在服务的onCreate()方法中将同步适配器组件实例化为单例。通过在onCreate()中实例化组件,您可以推迟创建组件,直到服务启动,这发生在框架首次尝试运行数据传输时。如果同步适配器框架响应触发器或调度对同步适配器进行多次执行排队,则需要以线程安全的方式实例化组件。

例如,以下代码片段向您展示了如何创建一个实现绑定Service的类,实例化您的同步适配器组件并获取 Android 绑定器对象。

Kotlin

package com.example.android.syncadapter
/**
 * Define a Service that returns an [android.os.IBinder] for the
 * sync adapter class, allowing the sync adapter framework to call
 * onPerformSync().
 */
class SyncService : Service() {
    /*
     * Instantiate the sync adapter object.
     */
    override fun onCreate() {
        /*
         * Create the sync adapter as a singleton.
         * Set the sync adapter as syncable
         * Disallow parallel syncs
         */
        synchronized(sSyncAdapterLock) {
            sSyncAdapter = sSyncAdapter ?: SyncAdapter(applicationContext, true)
        }
    }

    /**
     * Return an object that allows the system to invoke
     * the sync adapter.
     *
     */
    override fun onBind(intent: Intent): IBinder {
        /*
         * 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()
         *
         * We should never be in a position where this is called before
         * onCreate() so the exception should never be thrown
         */
        return sSyncAdapter?.syncAdapterBinder ?: throw IllegalStateException()
    }

    companion object {
        // Storage for an instance of the sync adapter
        private var sSyncAdapter: SyncAdapter? = null
        // Object to use as a thread-safe lock
        private val sSyncAdapterLock = Any()
    }
}

Java

package com.example.android.syncadapter;
/**
 * Define a Service that returns an <code><a href="/reference/android/os/IBinder.html">IBinder</a></code> 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();
    }
}

注意:要查看同步适配器的绑定服务的更详细示例,请参阅示例应用。

添加框架所需的帐户

同步适配器框架要求每个同步适配器都具有帐户类型。您在添加身份验证器元数据文件部分声明了帐户类型值。现在您必须在 Android 系统中设置此帐户类型。要设置帐户类型,请通过调用addAccountExplicitly()添加使用帐户类型的占位符帐户。

调用该方法的最佳位置是您应用的启动 Activity 的onCreate()方法。以下代码片段向您展示了如何执行此操作。

Kotlin

...
// Constants
// The authority for the sync adapter's content provider
const val AUTHORITY = "com.example.android.datasync.provider"
// An account type, in the form of a domain name
const val ACCOUNT_TYPE = "example.com"
// The account name
const val ACCOUNT = "placeholderaccount"
...
class MainActivity : FragmentActivity() {

    // Instance fields
    private lateinit var mAccount: Account
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       ...
        // Create the placeholder account
        mAccount = createSyncAccount()
       ...
    }
    ...
    /**
     * Create a new placeholder account for the sync adapter
     */
    private fun createSyncAccount(): Account {
        val accountManager = getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
        return Account(ACCOUNT, ACCOUNT_TYPE).also { 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, 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.
                 */
            }
        }
    }
    ...
}

Java

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 = "placeholderaccount";
    // Instance fields
    Account mAccount;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Create the placeholder account
        mAccount = CreateSyncAccount(this);
        ...
    }
    ...
    /**
     * Create a new placeholder 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.
             */
        }
    }
    ...
}

添加同步适配器元数据文件

要将同步适配器组件插入框架,您需要为框架提供描述组件并提供其他标志的元数据。元数据指定您为同步适配器创建的帐户类型,声明与您的应用关联的内容提供程序权限,控制与同步适配器相关的系统用户界面的一部分,并声明其他与同步相关的标志。在应用项目中/res/xml/目录中存储的特殊 XML 文件中声明此元数据。您可以为文件指定任何名称,尽管它通常称为syncadapter.xml

此 XML 文件包含单个 XML 元素<sync-adapter>,该元素具有以下属性。

android:contentAuthority
内容提供程序的 URI 权限。如果您在上一个课程创建存根内容提供程序中为您的应用创建了存根内容提供程序,请使用您为属性android:authorities指定的值,该属性位于您添加到应用清单中的<provider>元素中。此属性在在清单中声明提供程序部分中进行了更详细的说明。
如果您使用同步适配器将数据从内容提供程序传输到服务器,则此值应与您用于该数据的 Content URI 权限相同。此值也是您在android:authorities属性中指定的权限之一,该属性位于在应用清单中声明提供程序的<provider>元素中。
android:accountType
同步适配器框架所需的帐户类型。该值必须与您在创建身份验证器元数据文件时提供的帐户类型值相同,如添加身份验证器元数据文件部分所述。它也是您在添加框架所需的帐户部分中的代码片段中为常量ACCOUNT_TYPE指定的值。
设置属性
android:userVisible
设置同步适配器帐户类型的可见性。默认情况下,与帐户类型关联的帐户图标和标签在系统“设置”应用的“帐户”部分中可见,因此,除非您拥有与应用轻松关联的帐户类型或域,否则应使同步适配器不可见。如果使帐户类型不可见,您仍然可以通过应用的某个 Activity 中的用户界面允许用户控制同步适配器。
android:supportsUploading
允许您将数据上传到云端。如果您的应用仅下载数据,请将其设置为false
android:allowParallelSyncs
允许同时运行同步适配器组件的多个实例。如果您的应用支持多个用户帐户,并且您希望允许多个用户并行传输数据,请使用此选项。如果从未运行过多个数据传输,则此标志无效。
android:isAlwaysSyncable
指示同步适配器框架,它可以在您指定的任何时间运行您的同步适配器。如果您想以编程方式控制同步适配器何时可以运行,请将此标志设置为false,然后调用requestSync()来运行同步适配器。要详细了解如何运行同步适配器,请参阅课程运行同步适配器

以下示例显示了一个使用单个占位符帐户并且仅执行下载的同步适配器的 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"/>

在清单中声明同步适配器

将同步适配器组件添加到应用后,您必须请求与使用组件相关的权限,并且必须声明您添加的绑定Service

由于同步适配器组件运行在网络和设备之间传输数据的代码,因此您需要请求访问 Internet 的权限。此外,您的应用需要请求读取和写入同步适配器设置的权限,以便您可以从应用中的其他组件以编程方式控制同步适配器。您还需要请求一个特殊权限,允许您的应用使用您在上一个课程创建存根身份验证器中创建的身份验证器组件。

要请求这些权限,请将以下内容作为<manifest>的子元素添加到应用清单中。

android.permission.INTERNET
允许同步适配器代码访问 Internet,以便它可以从设备下载或上传数据到服务器。如果您之前已请求此权限,则无需再次添加。
android.permission.READ_SYNC_SETTINGS
允许您的应用读取当前的同步适配器设置。例如,您需要此权限才能调用getIsSyncable()
android.permission.WRITE_SYNC_SETTINGS
允许您的应用控制同步适配器设置。您需要此权限才能使用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"/>
    <uses-permission
            android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
...
</manifest>

最后,要声明框架用于与同步适配器交互的绑定Service,请将以下 XML 添加到应用清单中,作为<application>的子元素。

        <service
                android:name="com.example.android.datasync.SyncService"
                android:exported="false"
                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>元素设置了一个由意图操作android.content.SyncAdapter触发的过滤器,该过滤器由系统发送以运行同步适配器。触发过滤器时,系统将启动您创建的绑定服务,在本例中为SyncService。属性android:exported="false"仅允许您的应用和系统访问Service。属性android:process=":sync"告诉系统在名为sync的全局共享进程中运行Service。如果您的应用中有多个同步适配器,它们可以共享此进程,从而减少开销。

<meta-data> 元素提供之前创建的同步适配器元数据 XML 文件的名称。 android:name 属性指示此元数据适用于同步适配器框架。 android:resource 元素指定元数据文件的名称。

现在,您已经拥有了同步适配器所需的所有组件。下一课将向您展示如何指示同步适配器框架运行您的同步适配器,无论是在响应事件时还是按照定期计划运行。