Android NDK开发详解连接性之创建同步适配器
注意:对于大多数后台处理用例,我们建议使用 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
与触发同步适配器的事件相关联的 Account 对象。如果您的服务器不使用帐号,则不需要使用此对象中的信息。
Extras
这是一个 Bundle,包含由触发同步适配器的事件发送的标志。
Authority
系统中内容提供器的授权。您的应用必须拥有该提供程序的访问权限。通常,该授权与您的应用中的内容提供器相对应。
ContentProviderClient
授权参数所指向的内容提供器的 ContentProviderClient。ContentProviderClient 是内容提供器的轻量级公共接口。它具有与 ContentResolver 相同的基本功能。如果您使用内容提供器存储应用数据,则可以使用此对象连接到提供程序。否则,可以忽略它。
SyncResult
用于向同步适配器框架发送信息的 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 Binder 对象从同步适配器组件传递到框架。使用该 Binder 对象,框架可以调用 onPerformSync() 方法并将数据传递给它。
在该 Service 的 onCreate() 方法中将您的同步适配器组件实例化为单例。通过在 onCreate() 中实例化组件,您可以将组件创建推迟到该 Service 启动时,也就是框架首次尝试运行数据传输时。您需要以线程安全的方式实例化组件,以防同步适配器框架为响应触发器或调度而将多次同步适配器执行加入队列。
例如,以下代码段展示了如何创建相应的类以实现绑定 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 = "dummyaccount"
...
class MainActivity : FragmentActivity() {
// Instance fields
private lateinit var mAccount: Account
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// Create the dummy account
mAccount = createSyncAccount()
...
}
...
/**
* Create a new dummy 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 = "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 <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.
*/
}
}
...
}
添加同步适配器元数据文件
如需将您的同步适配器组件插入框架中,您需要为框架提供描述组件并提供其他标志的元数据。该元数据指定您为同步适配器创建的帐号类型,声明与应用关联的内容提供器授权,控制与同步适配器相关的系统界面部分,并声明其他与同步相关的标志。请在一个特殊的 XML 文件中声明该元数据,该文件存储在应用项目的 /res/xml/ 目录中。您可以为该文件指定任何名称,但通常将它命名为 syncadapter.xml。
该 XML 文件包含一个 XML 元素 ,该元素具有以下属性:
android:contentAuthority
您的内容提供器的 URI 授权。如果您在上一课创建桩内容提供器中为应用创建了桩内容提供器,请使用您为 android:authorities 属性指定的值,该属性位于您添加到应用清单中的 元素中。在清单中声明提供程序一节中对该属性进行了更详细的介绍。
如果您要使用同步适配器将数据从内容提供器传输到服务器,则该值应与您为该数据使用的内容 URI 授权相同。该值也是您在 元素的 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。
由于同步适配器组件运行的是在网络和设备间传输数据的代码,因此您需要请求网络访问权限。此外,您的应用需要请求读写同步适配器设置的权限,以便您可以通过编程方式从应用中的其他组件控制同步适配器。您还需要请求一个特殊的权限,以允许应用使用您在创建桩身份验证器一课中创建的身份验证器组件。
如需请求这些权限,请将以下内容作为 的子元素添加到应用清单中:
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 作为 的子元素添加到您的应用清单中:
<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>
元素设置由 intent 操作 android.content.SyncAdapter 触发的过滤器,系统发送此操作以运行同步适配器。当过滤器被触发时,系统会启动您创建的绑定服务,在本例中为 SyncService。android:exported=“true” 属性允许除您的应用之外的进程(包括系统)访问该 Service。属性 android:process=“:sync” 会指示系统在名为 sync 的全局共享进程中运行该 Service。如果您的应用中有多个同步适配器,则它们可以共享此进程,从而减少开销。
元素提供您之前创建的同步适配器元数据 XML 文件的名称。android:name 属性表明这是用于同步适配器框架的元数据。android:resource 元素指定该元数据文件的名称。
现在您已拥有同步适配器的所有组件。下节课将介绍如何指示同步适配器框架运行您的同步适配器,可以响应事件运行,也可以定期运行。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2020-06-26。