创建同步适配器

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

应用中的同步适配器组件封装了在设备和服务器之间传输数据的任务的代码。根据您在应用中提供的调度和触发器,同步适配器框架会运行同步适配器组件中的代码。如需向应用添加同步适配器组件,您需要添加以下几项内容:

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

本课将介绍如何定义这些元素。

创建同步适配器类

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

扩展同步适配器基类

如需创建同步适配器组件,请先扩展 AbstractThreadedSyncAdapter 并编写其构造函数。每次从头开始创建同步适配器组件时,都使用构造函数运行设置任务,就像使用 Activity.onCreate() 设置 Activity 一样。例如,如果您的应用使用 content provider 存储数据,请使用构造函数获取 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 对象。如果您的服务器不使用帐号,则不需要使用此对象中的信息。
Extra
一个 Bundle,包含由触发同步适配器的事件发送的标志。
职权
系统中 content provider 的授权。您的应用有权访问此提供程序。通常,该授权与您应用中的 content provider 相对应。
content provider 客户端
授权参数所指向的 content provider 的 ContentProviderClientContentProviderClient 是内容提供程序的轻量级公共接口。它具有与 ContentResolver 相同的基本功能。如果您使用 content provider 存储应用数据,则可以使用此对象连接到提供程序。否则,您可以忽略它。
同步结果
用于向同步适配器框架发送信息的 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() 的实际实现视应用的数据同步要求和服务器连接协议而定,但您的实现还应执行一些常规任务:

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

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

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

将同步适配器绑定到框架

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

在该服务的 onCreate() 方法中将您的同步适配器组件实例化为单例。通过在 onCreate() 中实例化组件,您可以将组件创建推迟到服务启动时,也就是框架首次尝试运行数据传输时。您需要以线程安全的方式实例化组件,以防同步适配器框架为响应触发器或调度而将多次同步适配器执行加入队列。

例如,以下代码段展示了如何创建一个类来实现绑定 Service、实例化同步适配器组件以及获取 Android Binder 对象:

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

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

如需将同步适配器组件插入到框架中,您需要为框架提供描述该组件并提供额外标志的元数据。该元数据指定您为同步适配器创建的帐号类型,声明与应用关联的 content provider 授权,控制与同步适配器相关的系统界面部分,并声明与同步相关的其他标记。请在一个特殊的 XML 文件中声明此元数据,该文件存储在应用项目的 /res/xml/ 目录中。您可以为该文件指定任何名称,但通常将它命名为 syncadapter.xml

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

android:contentAuthority
content provider 的 URI 授权。如果您在上一课创建桩内容提供器中为您的应用创建了桩内容提供器,请使用您在添加到应用清单的 <provider> 元素中为 android:authorities 属性指定的值。在清单中声明提供程序部分对此属性进行了更详细的说明。
如果您要使用同步适配器将数据从 content provider 传输到服务器,则此值应该与用于该数据的内容 URI 授权相同。此值也是您在 <provider> 元素的 android:authorities 属性中指定的授权之一,该元素用于在应用清单中声明您的提供程序。
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

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

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

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

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

现在您已拥有同步适配器的所有组件。下一课将介绍如何指示同步适配器框架运行您的同步适配器,可以响应事件运行,也可以定期运行。