14、联系人同步:SyncAdapter架构、Account与Authenticator、同步周期设置、处理同步冲突
说到联系人同步,很多开发者的第一反应就是「调个接口上传下载不就完了?」。嗯,如果你只是做个Demo,确实可以这么干。但一旦进入生产环境,你会发现事情远没那么简单——后台被系统杀掉怎么办?多账户怎么隔离?用户改了本地数据又改了服务器数据,到底听谁的?
Android官方其实早就为我们准备了一套完整的同步框架:SyncAdapter。这套东西说实话有点重,但一旦你理解了它的设计思路,就会发现它把同步这件事的「脏活累活」全包了。我个人习惯把它比作一个「同步管家」——你只需要告诉它什么时候该干活、怎么干活,剩下的调度、重试、电量优化,系统帮你搞定。
核心要点:SyncAdapter 不是让你直接写网络请求的,而是让你实现一个 ContentProvider 的同步扩展。它运行在独立的进程中,不会阻塞 UI,并且系统会根据网络状态、电量、用户设置来智能调度。
14.1 SyncAdapter 架构:谁在管谁?
先看一张整体架构图,我尽量画得直观一些。你不需要记住所有细节,但要知道每个模块的职责。
这张图里,SyncManager 是系统级的调度中心。它不关心你的业务逻辑,只负责在合适的时间调用你的 onPerformSync()。你的 SyncAdapter 则通过 ContentProvider 读写本地数据,并通过网络与服务器交互。AccountManager 和 Authenticator 负责账户的创建与认证——说白了,就是告诉系统「这个用户是谁,他有没有权限同步」。
我的经验:刚开始接触 SyncAdapter 时,我犯过一个低级错误——直接在 onPerformSync() 里做了网络请求,但没有检查网络状态。结果用户在飞行模式下,同步任务被反复触发,导致大量失败日志。后来我养成了习惯:在同步方法第一行就检查 ConnectivityManager,没有网络就直接返回,并设置 SyncResult 的重试标志。
14.2 Account 与 Authenticator:没有账户,同步个啥?
Android 的同步机制是「账户驱动」的。你想想看,如果系统不知道数据属于哪个用户,它怎么知道该同步到哪个服务器?所以,第一步是创建一个 Account 类型。
我通常会在 authenticator.xml 里定义账户类型,比如 com.example.contacts。然后实现一个 AbstractAccountAuthenticator 的子类。这里有个坑:Authenticator 的 addAccount() 方法 并不是真的去注册一个新用户,而是返回一个 Bundle,告诉系统「我已经处理了,你调用我的 Activity 去让用户输入账号密码吧」。
// 示例:Authenticator 的核心骨架
public class ContactAuthenticator extends AbstractAccountAuthenticator {
public ContactAuthenticator(Context context) {
super(context);
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) {
// 这里启动一个 Activity 让用户输入账号密码
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response,
Account account, String authTokenType,
Bundle options) {
// 从 AccountManager 获取缓存的 token,如果没有则重新登录
// 我曾经在这里踩过坑:没有处理 token 过期的情况
// 导致用户改了密码后,旧 token 一直用不了
return null; // 实际需要完整实现
}
// 其他方法:hasFeatures()、editProperties() 等
}
账户注册完成后,系统会在 Settings -> Accounts 里看到你的账户类型。用户点击「同步」按钮时,系统就会调用你的 SyncAdapter。
注意:Android 8.0 之后,系统对后台同步的限制越来越严。如果你的应用没有在前台运行,系统可能会延迟或合并同步请求。我曾经在适配 Android 12 时发现,addPeriodicSync() 的最小间隔被强制限制为 15 分钟,即使你设置成 5 分钟也没用。所以,别指望用同步来做实时推送。
14.3 同步周期设置:别让用户骂你耗电
同步周期的设置,说白了就是「多久拉一次数据」。但这里有个平衡:拉得太频繁,用户骂你耗电;拉得太慢,数据不一致。我个人习惯的做法是:
- 首次安装:立即触发一次全量同步,用
ContentResolver.requestSync()。 - 日常同步:使用
ContentResolver.addPeriodicSync(),间隔设为 1 小时。 - 用户手动下拉刷新:再次调用 requestSync(),并传入一个
SyncRequest.Builder设置syncExtras为true,表示这是用户主动触发的。
// 设置周期性同步
Account account = new Account("user@example.com", "com.example.contacts");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
ContentResolver.addPeriodicSync(
account,
ContactsContract.AUTHORITY,
Bundle.EMPTY,
3600L // 单位:秒,即1小时
);
这里有个细节:setSyncAutomatically() 必须设为 true,否则系统不会自动执行周期性同步。我曾经在一个项目里忘了这行代码,结果用户反馈「联系人从来不更新」,排查了半天才发现是这里的问题。
避坑指南:如果你需要支持「仅WiFi下同步」,可以在 SyncAdapter 的 onPerformSync() 里判断 ConnectivityManager 的网络类型。不要依赖系统提供的 setSyncable() 参数,因为不同厂商的定制系统对这个参数的处理不一致。我遇到过某款国产手机,即使设置了 SYNC_EXTRAS_REQUIRE_CHARGING,它依然在电量 20% 时执行同步。
14.4 处理同步冲突:到底听谁的?
同步冲突是联系人系统里最头疼的问题。用户可能在手机上修改了张三的电话号码,同时服务器上也有人修改了同一个联系人的邮箱。两边都变了,同步时该以谁为准?
常见的策略有三种:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 最后写入者胜出 | 比较时间戳,谁晚听谁的 | 简单场景,但依赖时钟同步 |
| 服务器优先 | 始终以服务器数据为准 | 企业级应用,数据由管理员维护 |
| 客户端优先 | 始终以本地修改为准 | 个人通讯录,用户本地操作频繁 |
| 合并+标记 | 保留双方修改,标记为「待确认」 | 需要用户手动裁决的场景 |
我个人比较推荐「最后写入者胜出 + 版本号校验」的组合。具体做法是:
- 每条联系人记录在本地和服务器都维护一个 version 字段(整数)。
- 同步时,客户端把本地的 version 发给服务器。
- 服务器比较 version:如果客户端的 version 小于服务器当前 version,说明服务器有更新,以服务器为准;反之则以客户端为准。
- 如果 version 相同但数据不同,说明两边同时修改了同一条记录——这时候我习惯用「时间戳」做二次判断,但时间戳不可靠时,就标记为冲突,让用户下次打开应用时手动选择。
// 冲突检测伪代码
public SyncResult resolveConflict(Contact local, Contact remote) {
if (local.version > remote.version) {
// 本地更新,上传本地数据
uploadContact(local);
return SyncResult.SYNC_UPLOAD;
} else if (local.version < remote.version) {
// 服务器更新,下载并覆盖本地
updateLocalContact(remote);
return SyncResult.SYNC_DOWNLOAD;
} else {
// version 相同,但数据不同 → 冲突
if (local.lastModified > remote.lastModified) {
uploadContact(local);
return SyncResult.SYNC_UPLOAD;
} else {
updateLocalContact(remote);
return SyncResult.SYNC_DOWNLOAD;
}
}
}
我的教训:早期我天真地只用时间戳来判断冲突,结果用户手机时间设置错误(比如跨时区没调),导致同步数据来回覆盖。后来我强制要求服务器返回一个单调递增的 version,客户端每次同步后更新本地 version。时间戳只作为辅助参考,不再作为唯一依据。这个改动之后,冲突率下降了 90%。
14.5 总结一下
SyncAdapter 这套框架,学习曲线确实有点陡。但一旦你掌握了它的套路,后续的维护成本会非常低。你不需要自己写 Service、不需要管理线程池、不需要担心系统杀掉进程——这些 SyncManager 都帮你做了。
最后提醒一句:别忘了在 AndroidManifest.xml 里声明你的 SyncAdapter 和 Authenticator,并且加上 android:exported="false"。我曾经见过有人忘了声明 <sync-adapter> 标签,结果同步服务死活不启动,查了两天才发现是清单文件的问题。
好了,这一章的内容就到这里。如果你在实际开发中遇到什么奇怪的同步问题,欢迎来公众号找我聊聊。
公众号:蓝海资料掘金营,微信 deep3321