一、数据同步任务的整体架构

说实话,数据同步这个需求,几乎每个App都会遇到。我做过好几个项目,从电商到社交,无一例外都要把本地数据往服务器上搬。但真正做好这件事,远比想象中复杂。

先看看我们要解决的核心问题:

  • 定期执行:不能用户一关App就不同步了
  • 冲突处理:本地改了,服务器也改了,听谁的?
  • 状态展示:用户得知道同步到哪一步了
  • 电量流量:不能为了同步把用户手机搞没电

我个人习惯把这类任务拆成三层:

  1. 调度层:WorkManager负责什么时候跑
  2. 执行层:真正的网络请求+数据比对
  3. 反馈层:把结果告诉UI
调度层 WorkManager PeriodicWorkRequest 执行层 Worker / CoroutineWorker doWork() 核心逻辑 反馈层 LiveData / Flow 同步状态观察 约束条件:网络可用、电量充足 核心逻辑:读取本地→比对→上传→处理结果 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