18、后台任务剖析:WorkManager任务监控、分析后台任务执行情况、优化后台调度

后台任务,说白了就是App在用户看不见的时候偷偷干活。但怎么干、什么时候干、干完了怎么知道结果,这里面的门道可不少。

我个人习惯用WorkManager来管理后台任务。为什么?因为它够聪明——能根据系统状态、电量、网络条件自动调整执行时机。但光会用还不够,你得会看、会分析、会优化。

18.1 WorkManager的任务监控

先说说怎么监控任务。WorkManager提供了WorkInfo这个类,它能告诉你任务的当前状态。

// 获取单个任务的WorkInfo
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(workRequest.id)
    .observe(this, { workInfo ->
        if (workInfo != null) {
            val state = workInfo.state
            Log.d("WorkManager", "任务状态: $state")
            // state可以是: ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCELLED
        }
    })

我在项目中遇到过一个问题:任务明明成功了,但UI没刷新。后来发现是忘了观察SUCCEEDED状态。你想想看,WorkManager的状态机其实是个有限状态机,每个状态转换都有明确规则。

核心状态流转:
ENQUEUED → RUNNING → SUCCEEDED
ENQUEUED → RUNNING → FAILED
ENQUEUED → BLOCKED → ENQUEUED
ENQUEUED → CANCELLED

18.2 分析后台任务执行情况

光看状态还不够,你得知道任务跑了多久、耗了多少电、占了多少网络。WorkManager 2.7+版本引入了WorkManager Metrics,可以收集这些数据。

// 在Application中初始化
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        WorkManager.initialize(this, Configuration.Builder()
            .setMinimumLoggingLevel(android.util.Log.DEBUG)
            .build())
    }
}

嗯,这里要注意:Metrics数据默认不开启,需要你在AndroidManifest.xml里加个meta-data。

<meta-data
    android:name="androidx.work.WorkManagerMetrics"
    android:value="true" />

我曾经踩过一个坑:开了Metrics后,发现某个任务平均执行时间从2秒飙到了15秒。查了半天,原来是任务里做了网络请求,但网络条件差的时候一直在重试。说白了,就是没设置合理的超时和重试策略。

我的调试小技巧:adb shell dumpsys jobscheduler可以查看系统级别的Job调度情况,配合WorkManager的日志,基本能定位90%的问题。

18.3 优化后台调度

优化调度,核心就一句话:别跟系统对着干。系统说现在电量低,你就别跑大任务;系统说网络是计费网络,你就别下载大文件。

WorkManager提供了Constraints类,让你优雅地表达任务执行条件。

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)  // 只在非计费网络执行
    .setRequiresBatteryNotLow(true)                 // 电量不能低
    .setRequiresCharging(true)                      // 最好在充电时执行
    .setRequiresStorageNotLow(true)                 // 存储空间充足
    .build()

val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(constraints)
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        10, TimeUnit.SECONDS
    )
    .build()

为什么用指数退避?你想想看,如果网络不好,你每秒重试一次,只会让情况更糟。指数退避的意思是:第一次等10秒,第二次等20秒,第三次等40秒……给系统留出恢复时间。

我曾经犯过的错: 给一个图片上传任务设置了NetworkType.CONNECTED,结果用户在地铁里信号断断续续,任务反复启动又失败,电量哗哗地掉。后来改成UNMETERED + 充电条件,问题就解决了。

18.4 任务链与并行调度

实际项目中,很少有单个任务就能搞定的。比如用户上传头像:先压缩,再上传,最后更新本地数据库。这三个步骤有依赖关系,但压缩和上传可以并行吗?不行,得先压缩再上传。

// 创建任务链
val compress = OneTimeWorkRequestBuilder<CompressWorker>()
    .addTag("compress")
    .build()

val upload = OneTimeWorkRequestBuilder<UploadWorker>()
    .addTag("upload")
    .build()

val updateDb = OneTimeWorkRequestBuilder<UpdateDbWorker>()
    .addTag("updateDb")
    .build()

WorkManager.getInstance(context)
    .beginWith(compress)
    .then(upload)
    .then(updateDb)
    .enqueue()

这里有个细节:beginWith可以传多个WorkRequest,它们会并行执行。比如同时压缩多张图片:

val compress1 = OneTimeWorkRequestBuilder<CompressWorker>().build()
val compress2 = OneTimeWorkRequestBuilder<CompressWorker>().build()
val compress3 = OneTimeWorkRequestBuilder<CompressWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(compress1, compress2, compress3))
    .then(upload)
    .enqueue()

并行度怎么控制?WorkManager默认会根据系统负载自动调整。但我建议你别开太多并行任务,3-5个就差不多了。开太多反而会因为线程切换导致性能下降。

18.5 知识体系总览

下面这张图是我自己总结的WorkManager后台任务剖析全景图,涵盖了监控、分析和优化三个维度。

WorkManager后台任务剖析全景图 任务监控 • WorkInfo状态观察 • LiveData/Flow监听 • 状态机流转分析 • 任务链追踪 • 日志与dumpsys 执行分析 • Metrics数据收集 • 执行时长统计 • 电量消耗分析 • 网络使用情况 • 失败原因定位 调度优化 • Constraints约束 • 指数退避策略 • 任务链编排 • 并行度控制 • 系统资源感知 监控 → 分析 → 优化,形成闭环迭代

18.6 实战建议

最后,分享几个我在实战中总结的经验:

  • 别滥用PeriodicWorkRequest:最小间隔是15分钟,但系统可能会合并多个周期任务。如果你需要精确的定时,考虑用AlarmManager。
  • 任务结果要持久化:WorkManager的任务结果存在Room数据库里,但应用进程被杀后,WorkInfo的LiveData可能收不到回调。我习惯在Worker的doWork()里直接把结果写进数据库。
  • 测试要覆盖边界情况:比如飞行模式、低电量、存储空间不足。WorkManager提供了TestListenableWorkerBuilder,可以模拟各种约束条件。
  • 日志要分级:开发环境用Log.DEBUG,线上环境用Log.INFO。别把敏感信息打出来。
一句话总结: WorkManager不是黑盒,它给了你足够的工具去监控、分析和优化。关键是你得用起来,别等到用户反馈"App耗电"才去查。

好了,关于后台任务剖析就聊到这儿。记住,好的后台调度不是让任务跑得最快,而是让任务在最合适的时候跑。这才是对用户负责的做法。