27、Content Provider 与 SyncAdapter 整合:账户同步、自动同步、冲突解决

说实话,Content Provider 单独用其实挺简单的。但一旦涉及到「数据需要和服务器同步」,事情就变得有意思了。我早期做过一个笔记应用,用户离线写笔记,上线后要自动同步到云端。当时我直接用 Service 轮询,结果被用户骂成狗——耗电、流量、同步冲突,各种问题。

后来我才真正搞明白,Android 官方其实给了我们一套完整的方案:SyncAdapter。它不是一个库,而是一个框架。说白了,就是系统帮你管理同步的时机、频率、网络状态,你只需要写好「怎么同步」和「怎么解决冲突」就行。

27.1 为什么需要 SyncAdapter?

你想想看,如果让你自己写同步逻辑,你会怎么做?

  • 开一个 Service,每隔几分钟拉一次数据?—— 太耗电
  • 监听网络变化,连上就同步?—— 逻辑容易乱
  • 用户手动点击「同步」按钮?—— 体验太差

SyncAdapter 解决了三个核心痛点:

  1. 自动触发:系统会根据网络、电量、用户设置,智能决定何时同步
  2. 统一管理:所有 Content Provider 的同步都可以通过「设置 → 账户」统一控制
  3. 生命周期可控:系统会绑定一个 Service 来运行 SyncAdapter,不会常驻后台

核心思想:SyncAdapter 不是让你「主动去同步」,而是让系统「在合适的时候调用你」。你只需要实现一个抽象类,告诉系统「怎么同步」就行。

27.2 账户同步:先有个身份

SyncAdapter 依赖 Android 的账户系统。什么意思?就是你的应用需要先注册一个「账户类型」,比如 com.example.myapp.account。系统看到这个账户,才知道「哦,这个应用需要同步」。

我记得第一次做账户同步时,踩了一个坑:账户必须和 Authenticator 绑定。你不能只写一个 SyncAdapter,还得写一个 AuthenticatorService。哪怕你不需要真正的账户认证,也得提供一个「假的」Authenticator。

来看一下完整的账户注册流程:

// 1. 定义账户类型(在 res/xml/authenticator.xml 中)
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.example.myapp.account"
    android:icon="@mipmap/ic_launcher"
    android:smallIcon="@mipmap/ic_launcher_small"
    android:label="@string/app_name" />

// 2. 实现 Authenticator(哪怕什么都不做)
public class MyAuthenticator extends AbstractAccountAuthenticator {
    public MyAuthenticator(Context context) {
        super(context);
    }

    @Override
    public Bundle addAccount(...) {
        // 返回 null 表示不需要交互式添加
        return null;
    }

    // 其他方法直接返回 null 或抛出异常即可
}

// 3. 在 AndroidManifest 中注册
<service
    android:name=".sync.MyAuthenticatorService"
    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>

我的习惯:我会在 Application 的 onCreate 中检查账户是否存在,如果不存在就自动添加一个。这样用户不需要手动去「设置 → 账户」里添加。

27.3 自动同步:让系统帮你调度

账户有了,接下来就是 SyncAdapter 本身。你需要继承 AbstractThreadedSyncAdapter,实现 onPerformSync 方法。这个方法会在后台线程被调用,你在这里写真正的同步逻辑。

public class MySyncAdapter extends AbstractThreadedSyncAdapter {
    public MySyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
    }

    @Override
    public void onPerformSync(
            Account account,
            Bundle extras,
            String authority,
            ContentProviderClient providerClient,
            SyncResult syncResult) {

        // 1. 从 ContentProvider 读取本地数据
        // 2. 从服务器拉取远程数据
        // 3. 对比差异,解决冲突
        // 4. 写回 ContentProvider

        // 注意:这里已经是后台线程,可以直接做网络请求
    }
}

然后注册 SyncAdapter Service:

<service
    android:name=".sync.MySyncService"
    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>

syncadapter.xml 中需要指定账户类型和 ContentProvider 的 authority:

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

注意android:allowParallelSyncs 建议设为 false。我曾经设成 true,结果多个同步线程同时写同一个 ContentProvider,数据乱成一锅粥。除非你的 Provider 做了完善的并发控制,否则别开。

27.4 冲突解决:谁才是对的?

同步最头疼的问题就是冲突。用户离线修改了一条记录,服务器上也有人修改了同一条记录。等用户上线同步时,到底以谁为准?

我总结了几种常见的冲突策略:

策略 说明 适用场景
最后写入者胜 比较时间戳,谁晚听谁的 简单场景,但依赖时钟同步
服务器优先 以服务器数据为准,覆盖本地 服务端是权威数据源
客户端优先 以本地数据为准,覆盖服务器 离线编辑为主的应用
合并策略 尝试合并两个版本的数据 字段级冲突,比如联系人合并
标记冲突 不自动解决,让用户手动选择 关键数据,不能丢失

我个人比较推荐「最后写入者胜 + 标记冲突」的组合。什么意思呢?就是默认用时间戳决定,但如果两个版本的时间戳非常接近(比如相差不到 1 秒),就标记为冲突,让用户决定。

来看一个具体的冲突检测代码:

// 假设每条记录都有 lastModified 字段
public class SyncConflictResolver {

    public static SyncResult resolve(
            ContentValues local,
            ContentValues server,
            long conflictThresholdMs) {

        long localTime = local.getAsLong("last_modified");
        long serverTime = server.getAsLong("last_modified");

        // 时间差在阈值内,标记为冲突
        if (Math.abs(localTime - serverTime) < conflictThresholdMs) {
            // 写入冲突表,等待用户处理
            insertConflictRecord(local, server);
            return SyncResult.SYNC_CONFLICT;
        }

        // 否则以较新的为准
        if (localTime > serverTime) {
            return SyncResult.LOCAL_WINS;
        } else {
            return SyncResult.SERVER_WINS;
        }
    }
}

避坑指南:我曾经在冲突表中存了完整的两份数据,结果用户量一大,冲突表比主表还大。后来我改成只存「冲突字段的差异」,而不是整条记录。这样既节省空间,用户解决冲突时也更容易看清差异。

27.5 知识体系总览

下面这张图把整个 SyncAdapter 的架构串起来了。你可以看到,从账户注册到同步触发,再到冲突解决,是一条完整的链路。

SyncAdapter 完整架构 账户系统 AccountManager + Authenticator SyncAdapter 框架 AbstractThreadedSyncAdapter ContentProvider 本地数据存储 同步触发方式 自动同步(周期性) ContentResolver.addPeriodicSync() 即时同步(手动) ContentResolver.requestSync() 数据变更同步 ContentResolver.notifyChange() 冲突解决策略 最后写入者胜 服务器优先 客户端优先 标记冲突 同步完成 / 冲突待处理

27.6 实战中的几个建议

嗯,最后分享几个我在实战中总结的经验:

  • 不要在主线程做任何同步操作。onPerformSync 虽然已经在后台线程,但如果你在里面调了 ContentProvider 的 query,记得用异步方式,或者控制好游标不要泄漏。
  • 善用 SyncResult。这个对象可以告诉系统「同步遇到了延迟错误」还是「发生了冲突」。系统会根据这个决定是否重试。我曾经忽略了 SyncResult,结果同步失败后系统一直重试,用户流量哗哗的。
  • 测试时手动触发同步。用 adb 命令 adb shell content request-sync 可以强制触发同步,不用等系统调度。调试效率高很多。

我的习惯:我会在 ContentProvider 的 insert/update 方法中调用 notifyChange,并设置 syncToNetwork 为 true。这样每次本地数据变化,系统都会自动触发一次同步。用户完全无感知,体验很好。

好了,SyncAdapter 的核心内容就这些。说白了,它就是一套「让系统帮你管理同步」的框架。你只需要写好账户、SyncAdapter、冲突解决,剩下的交给系统。别像我早期那样自己写轮询 Service,费力不讨好。

公众号:蓝海资料掘金营,微信deep3321