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后台任务剖析全景图,涵盖了监控、分析和优化三个维度。
18.6 实战建议
最后,分享几个我在实战中总结的经验:
- 别滥用PeriodicWorkRequest:最小间隔是15分钟,但系统可能会合并多个周期任务。如果你需要精确的定时,考虑用AlarmManager。
- 任务结果要持久化:WorkManager的任务结果存在Room数据库里,但应用进程被杀后,
WorkInfo的LiveData可能收不到回调。我习惯在Worker的doWork()里直接把结果写进数据库。 - 测试要覆盖边界情况:比如飞行模式、低电量、存储空间不足。WorkManager提供了
TestListenableWorkerBuilder,可以模拟各种约束条件。 - 日志要分级:开发环境用
Log.DEBUG,线上环境用Log.INFO。别把敏感信息打出来。
好了,关于后台任务剖析就聊到这儿。记住,好的后台调度不是让任务跑得最快,而是让任务在最合适的时候跑。这才是对用户负责的做法。