数据库与WorkManager:让后台任务像瑞士钟表一样精准

说实话,做Android开发这么多年,我最怕听到的一句话就是:「用户反馈App越来越卡,数据还丢了」。排查下来,十有八九是数据库维护没做好。今天我们就来聊聊如何用WorkManager这把瑞士军刀,把数据库的脏活累活安排得明明白白。

为什么需要WorkManager来管理数据库?

你想想看,数据库就像你的衣柜。平时往里塞数据很容易,但时间久了,旧衣服(过期数据)不清理,衣柜就炸了。手动清理?用户可没这个耐心。用Service?Android系统会杀后台。用JobScheduler?兼容性又是个坑。

WorkManager就是官方给出的标准答案。它能在各种Android版本上稳定运行,还能处理设备重启、省电模式等极端情况。我个人习惯把所有数据库维护任务都交给它,省心。

定期清理数据库:给数据做「断舍离」

我在项目中遇到过这样一个场景:聊天App的本地消息表,三个月就涨到了500MB。用户手机存储告急,差评如潮。解决方案就是定期清理。

核心思路:使用PeriodicWorkRequest设置定期任务,比如每天凌晨清理7天前的日志数据。

// 定义清理Worker
class CleanupWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        val db = AppDatabase.getInstance(applicationContext)
        val cutoff = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L
        db.messageDao().deleteOldMessages(cutoff)
        Log.d("Cleanup", "已清理 $cutoff 之前的消息")
        return Result.success()
    }
}

// 创建定期任务
val cleanupRequest = PeriodicWorkRequestBuilder<CleanupWorker>(
    24, TimeUnit.HOURS
).setConstraints(
    Constraints.Builder()
        .setRequiresBatteryNotLow(true)  // 电量不低于低电量阈值
        .build()
).build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "db_cleanup",
    ExistingPeriodicWorkPolicy.KEEP,
    cleanupRequest
)

避坑指南:我曾经把清理间隔设成1小时,结果用户手机电量哗哗掉。后来改成24小时+电量约束,再也没收到投诉。记住:数据库操作是IO密集型,频繁执行会加速电池消耗。

数据同步后台任务:让云端和本地「握手」

数据同步是个老生常谈的话题。用户离线时写本地,联网后同步到服务器。这里有个关键点:同步失败怎么办?重试策略怎么定?

WorkManager提供了指数退避重试,说白了就是第一次失败等10秒,第二次等20秒,第三次等40秒……直到成功或放弃。我建议最多重试3次,超过就标记为失败,等下次周期任务再处理。

class SyncWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        return try {
            val pendingSyncs = SyncDao.getPendingSyncs()
            for (sync in pendingSyncs) {
                val response = apiService.syncData(sync.data)
                if (response.isSuccessful) {
                    SyncDao.markSynced(sync.id)
                } else {
                    return Result.retry()  // 触发重试
                }
            }
            Result.success()
        } catch (e: IOException) {
            Result.retry()  // 网络异常也重试
        }
    }
}

// 设置重试策略
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        10, TimeUnit.SECONDS
    )
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)  // 必须有网络
            .build()
    ).build()

注意:重试次数不是无限的。默认WorkManager会重试3次,之后标记为失败。你可以通过setBackoffCriteria调整,但别超过5次,否则用户会看到「同步失败」的弹窗,体验很差。

WorkManager链式任务:像流水线一样工作

有时候一个任务依赖另一个任务的结果。比如:先清理数据库,再同步数据,最后生成统计报告。这就是链式任务。

我个人特别喜欢这种模式,它把复杂的业务流程拆解成一个个小任务,每个任务只做一件事。出了问题也容易定位。

// 定义三个Worker
class CleanWorker : Worker() { /* 清理逻辑 */ }
class SyncWorker : Worker() { /* 同步逻辑 */ }
class ReportWorker : Worker() { /* 生成报告 */ }

// 构建链式任务
val chain = WorkManager.getInstance(context)
    .beginWith(OneTimeWorkRequest.from(CleanWorker::class.java))
    .then(OneTimeWorkRequest.from(SyncWorker::class.java))
    .then(OneTimeWorkRequest.from(ReportWorker::class.java))

chain.enqueue()

你可能会问:如果中间某个任务失败了怎么办?默认情况下,链式任务会中断,后续任务不会执行。但你可以通过beginUniqueWork设置策略,比如KEEP保留已有链,或者REPLACE替换。

约束条件:别让App成为「电老虎」

数据库操作很耗电,尤其是大量写入时。WorkManager提供了丰富的约束条件,我总结了一张表:

约束条件 说明 适用场景
NetworkType 网络类型(WIFI/移动数据/不限) 数据同步、大文件下载
BatteryNotLow 电量不低于低电量阈值 数据库清理、索引重建
StorageNotLow 存储空间充足 数据库备份、日志导出
IdleRequired 设备处于空闲状态 数据库VACUUM、碎片整理

我的经验:数据库VACUUM(整理碎片)一定要加IdleRequired约束。因为VACUUM会锁表,如果用户正在操作App,会导致界面卡顿。我曾经没加这个约束,结果用户反馈「App突然不动了」,排查半天才发现是VACUUM在搞鬼。

数据库维护自动化:打造「无人值守」的维护系统

把上面所有知识点整合起来,就是一个完整的自动化维护系统。我画了一张流程图,帮你理清思路:

数据库自动化维护流程 WorkManager启动 检查约束条件(网络/电量/存储) 链式任务:清理过期数据 链式任务:同步待上传数据 任务完成

这个流程看起来简单,但实际落地时要注意几个细节:

  • 任务唯一性:使用enqueueUniquePeriodicWork确保同一时间只有一个清理任务在运行,避免数据竞争。
  • 日志记录:每个Worker执行前后都打日志,方便排查问题。我习惯用Log.d记录开始和结束时间。
  • 异常处理:数据库操作可能抛出SQLiteException,一定要捕获并返回Result.failure(),否则WorkManager会认为任务还在运行,导致后续任务阻塞。

重要提醒:不要在Worker里持有Activity或Fragment的引用。Worker运行在后台线程,生命周期不受UI控制。如果你需要更新UI,用LiveData或Flow监听WorkInfo的状态变化。

实战中的那些坑

最后分享几个我踩过的坑,希望能帮你少走弯路:

  1. 数据库版本升级时,WorkManager任务会中断。 因为升级过程中数据库被锁定,Worker执行会失败。解决方案:在升级回调中暂停所有WorkManager任务,升级完成后再恢复。
  2. 不要用OneTimeWorkRequest做定期任务。 我见过有人用AlarmManager+OneTimeWorkRequest模拟定期任务,结果设备重启后任务丢失。直接用PeriodicWorkRequest,系统会帮你处理重启恢复。
  3. 约束条件不是万能的。 比如BatteryNotLow在Android 8.0以下不支持。记得做版本判断,或者使用Constraints.Builder().apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) setRequiresBatteryNotLow(true) }

好了,关于数据库与WorkManager的结合就聊到这里。这套方案我在多个项目中使用过,稳定运行超过两年,没出过问题。你完全可以放心使用。

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