延迟执行与重试策略:让后台任务更智能

后台任务调度,说白了就是让 App 在合适的时间干合适的活。但现实往往没那么简单——网络可能不稳定,服务器可能暂时抽风,用户可能正在用手机打游戏。这时候,延迟执行和重试策略就成了我们的救命稻草。

我个人习惯把 WorkManager 的调度策略比作「闹钟+保险丝」的组合。闹钟决定什么时候响,保险丝决定出问题了怎么补救。今天我们就来拆解这两个核心机制。

初始延迟设置:让任务「等一下」

先说说 setInitialDelay。这个 API 的作用很简单——让任务在指定的时间之后才开始执行。我在项目中遇到过这样一个场景:用户上传照片后,我们希望在 5 分钟后才开始压缩处理,避免影响用户当前的操作体验。

val workRequest = OneTimeWorkRequestBuilder<CompressWorker>()
    .setInitialDelay(5, TimeUnit.MINUTES)
    .build()

WorkManager.getInstance(context).enqueue(workRequest)

嗯,这里要注意:setInitialDelay 只对 OneTimeWorkRequest 有效。对于 PeriodicWorkRequest,你需要用 setInitialDelay 来设置第一次执行的延迟时间,但后续的周期执行不受影响。

小技巧:如果你想让任务在特定时间点执行(比如凌晨 2 点),别用 setInitialDelay 硬算时间差。我建议用 setInitialDelay 配合 Constraints 来实现更精准的调度。

重试与退避策略:失败后的优雅处理

任务执行失败了怎么办?直接放弃?还是立刻重试?WorkManager 给了我们一套非常灵活的重试机制。

首先,要让任务支持重试,你需要在 WorkerdoWork() 方法中返回 Result.retry()

class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        return try {
            // 执行上传操作
            uploadFile()
            Result.success()
        } catch (e: IOException) {
            // 网络异常,需要重试
            Result.retry()
        }
    }
}

但问题来了:如果网络一直不好,任务会无限重试下去吗?当然不会。WorkManager 默认有重试次数限制,而且每次重试之间会有间隔——这就是退避策略。

线性退避 vs 指数退避

WorkManager 提供了两种退避策略:

策略 说明 重试间隔 适用场景
BackoffPolicy.LINEAR 线性退避 每次增加固定时间 临时性故障(如服务器短暂过载)
BackoffPolicy.EXPONENTIAL 指数退避 每次翻倍增长 持续性故障(如网络断开)

举个例子你就明白了。假设初始延迟是 10 秒:

  • 线性退避:第 1 次重试等 10 秒,第 2 次等 20 秒,第 3 次等 30 秒……
  • 指数退避:第 1 次重试等 10 秒,第 2 次等 20 秒,第 3 次等 40 秒,第 4 次等 80 秒……

你想想看,如果服务器正在重启,指数退避能避免客户端疯狂请求造成雪崩效应。我曾经在一个项目中遇到过这种情况:服务器凌晨做维护,客户端每 5 秒重试一次,结果服务器刚恢复就被请求淹没了。后来改成指数退避,问题就解决了。

核心要点:指数退避更适合「不确定什么时候能恢复」的场景,线性退避更适合「我知道问题很快就能解决」的场景。

最大重试次数限制

WorkManager 默认的最大重试次数是 3 次(包括第一次执行)。超过这个次数后,任务会被标记为失败,不再重试。

但你可以通过 setBackoffCriteria 来调整:

val workRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        WorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .build()

这里有个坑:WorkRequest.MIN_BACKOFF_MILLIS 是最小退避时间,默认是 10 秒。你不能设置比这个更小的值,否则 WorkManager 会强制使用最小值。我曾经想设成 1 秒,结果发现根本不生效——嗯,Google 工程师早就想到了这种滥用场景。

避坑指南:我曾经在项目中把退避时间设得太短(比如 1 秒),结果任务在短时间内重试了十几次,不仅耗电,还导致服务器压力剧增。建议最小退避时间不要低于 10 秒。

知识体系总览

下面这张图帮你理清延迟执行与重试策略的核心逻辑:

延迟执行与重试策略知识体系 WorkManager 调度策略 初始延迟设置 (setInitialDelay) 仅对 OneTimeWorkRequest 有效 重试与退避策略 线性退避 (LINEAR) 指数退避 (EXPONENTIAL) 最大重试次数限制 Result.retry() 触发重试,Result.success() 结束任务

实战建议

说了这么多理论,最后分享几个我在实战中总结的经验:

  • 网络请求场景:用指数退避 + 最小延迟 10 秒。网络波动通常不会持续太久,指数增长能有效降低服务器压力。
  • 文件上传场景:用线性退避 + 最小延迟 30 秒。文件上传失败往往是因为文件过大或网络不稳定,线性增长更可控。
  • 数据同步场景:结合 Constraints 使用。比如只在 Wi-Fi 下重试,避免消耗用户流量。
我的习惯:每次设置重试策略时,我都会问自己三个问题:这个任务失败后多久能恢复?重试太频繁会不会影响用户体验?服务器能承受多大的重试压力?想清楚这些,策略自然就出来了。

好了,延迟执行和重试策略就讲到这里。记住一句话:好的调度策略,不是让任务「跑得快」,而是让任务「跑得稳」。下次遇到后台任务失败的情况,你应该知道怎么处理了吧?

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