一、数据同步任务的整体架构
说实话,数据同步这个需求,几乎每个App都会遇到。我做过好几个项目,从电商到社交,无一例外都要把本地数据往服务器上搬。但真正做好这件事,远比想象中复杂。
先看看我们要解决的核心问题:
- 定期执行:不能用户一关App就不同步了
- 冲突处理:本地改了,服务器也改了,听谁的?
- 状态展示:用户得知道同步到哪一步了
- 电量流量:不能为了同步把用户手机搞没电
我个人习惯把这类任务拆成三层:
- 调度层:WorkManager负责什么时候跑
- 执行层:真正的网络请求+数据比对
- 反馈层:把结果告诉UI
核心思路:WorkManager只负责「什么时候干活」,真正的业务逻辑全在Worker里。这样调度和业务解耦,测试起来也方便。
二、定义数据同步的Worker
嗯,这里要注意。很多人一上来就写Worker,结果发现同步逻辑和WorkManager耦合太紧。我建议把同步逻辑抽成一个独立的Repository。
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 1. 检查约束条件
if (!hasNetwork()) {
return Result.retry()
}
// 2. 读取本地待同步数据
val localData = syncRepository.getPendingSyncItems()
if (localData.isEmpty()) {
return Result.success()
}
// 3. 逐条同步
for (item in localData) {
val result = syncRepository.syncToServer(item)
when (result) {
is SyncResult.Success -> {
syncRepository.markAsSynced(item.id)
}
is SyncResult.Conflict -> {
handleConflict(item, result.serverData)
}
is SyncResult.Failure -> {
// 记录失败,下次重试
syncRepository.markAsFailed(item.id, result.error)
}
}
}
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
我的经验:用CoroutineWorker而不是普通Worker,因为同步操作基本都是IO密集型的。协程可以让你用同步的写法处理异步逻辑,代码清晰很多。
三、冲突处理策略
冲突处理是数据同步里最头疼的问题。我在项目中遇到过好几次,本地数据和服务器数据不一致,结果同步完数据全乱了。
常见的策略有这几种:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 客户端优先 | 以本地数据为准,覆盖服务器 | 草稿、本地编辑为主 |
| 服务端优先 | 以服务器数据为准,丢弃本地 | 配置同步、只读数据 |
| 时间戳比对 | 谁最后修改就用谁的 | 通用场景 |
| 手动合并 | 把冲突交给用户决定 | 重要数据、不可自动处理 |
我个人比较推荐时间戳比对,简单有效。但要注意一点:时间戳必须用服务器时间,不能用本地时间。为什么?因为用户可能改了手机时间,你想想看,那不乱套了吗?
data class SyncItem(
val id: String,
val localData: String,
val localTimestamp: Long,
val serverTimestamp: Long? = null
)
fun resolveConflict(local: SyncItem, server: SyncItem): SyncItem {
// 服务器时间戳为空,说明是新增数据
if (server.serverTimestamp == null) {
return local
}
// 谁的时间戳大,就用谁的
return if (local.localTimestamp > server.serverTimestamp) {
local // 本地更新,上传覆盖
} else {
server // 服务器更新,拉取覆盖
}
}
避坑指南:我曾经在时间戳比对时用了本地时间,结果用户把手机时间调快了一小时,所有数据都被覆盖了。从那以后,我每次同步都会先请求服务器时间,再拿这个时间戳去比对。
四、同步状态展示
用户需要知道同步进行到哪一步了。我习惯用LiveData或者Flow来暴露状态。
sealed class SyncState {
object Idle : SyncState()
object Syncing : SyncState()
data class Success(val count: Int) : SyncState()
data class Failure(val error: String) : SyncState()
data class Conflict(val itemId: String) : SyncState()
}
class SyncViewModel : ViewModel() {
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
fun startSync() {
viewModelScope.launch {
_syncState.value = SyncState.Syncing
val workManager = WorkManager.getInstance(application)
val workInfo = workManager
.getWorkInfosByTagLiveData("data_sync")
.asFlow()
.first()
when (workInfo.first().state) {
WorkInfo.State.SUCCEEDED -> {
_syncState.value = SyncState.Success(10)
}
WorkInfo.State.FAILED -> {
_syncState.value = SyncState.Failure("同步失败,请重试")
}
else -> {
_syncState.value = SyncState.Idle
}
}
}
}
}
关键点:状态展示要实时更新。WorkManager的WorkInfo是LiveData,可以直接观察。但要注意,WorkInfo只在任务执行期间有变化,任务结束后状态就固定了。
五、电量与流量优化
说白了,用户最烦的就是App偷偷跑流量、耗电。我见过一个App,后台同步每小时跑一次,每次同步几百KB,一个月下来流量跑了几百兆。用户不卸载才怪。
优化策略:
- 合并请求:多条数据合并成一次网络请求,减少连接次数
- 增量同步:只同步变化的数据,不要全量上传
- 压缩数据:JSON数据用Gzip压缩,能省一半流量
- 合理间隔:不要设太短的同步间隔,15分钟以上比较合理
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 有网络才同步
.setRequiresBatteryNotLow(true) // 电量不能太低
.setRequiresCharging(false) // 不强制充电
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES // 15分钟间隔
)
.setConstraints(constraints)
.addTag("data_sync")
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"data_sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
我的建议:用ExistingPeriodicWorkPolicy.KEEP而不是REPLACE。因为如果用户已经有一个同步任务在排队,你REPLACE的话,之前的任务会被取消,可能丢失数据。KEEP会保留原有任务,新任务会被忽略。
还有一个容易被忽略的点:数据去重。如果本地数据频繁变化,每次同步都上传全部数据,流量消耗会很大。我一般会在本地维护一个「变更日志」,只记录哪些数据变了,同步时只上传变更的部分。
// 变更日志示例
data class ChangeLog(
val itemId: String,
val changeType: ChangeType, // CREATE, UPDATE, DELETE
val timestamp: Long
)
// 同步时只取变更日志中的数据
fun getChangedItems(): List<SyncItem> {
val changes = changeLogDao.getUnsyncedChanges()
return changes.map { change ->
val item = localDataDao.getItem(change.itemId)
SyncItem(
id = item.id,
localData = item.toJson(),
localTimestamp = change.timestamp
)
}
}
嗯,到这里,数据同步任务的核心内容就差不多了。从架构设计到Worker实现,从冲突处理到状态展示,再到电量流量优化,每一步都有不少坑。但只要你把基础打牢了,后面遇到复杂场景也能从容应对。
公众号:蓝海资料掘金营,微信deep3321