21、WorkManager的前台服务:setForeground()方法、创建ForegroundInfo、前台服务的生命周期管理、Android 12+的前台服务限制
说实话,后台任务调度这块,最让人头疼的就是「保活」问题。你想想看,用户把App切到后台,系统随时可能把你的任务给杀了。尤其是Android 12之后,限制越来越严,光靠WorkManager的默认Worker,有时候真不够用。
那怎么办?前台服务就是答案。说白了,就是给你的后台任务「加个护身符」——让系统知道,这个任务很重要,用户也能看到通知栏里有个提示。
我个人习惯,凡是需要长时间运行、用户能感知到的任务(比如文件上传、数据同步),都会考虑用前台服务。今天我们就来聊聊,WorkManager里怎么玩转前台服务。
setForeground()方法:让Worker「前台化」
在WorkManager里,想让一个Worker变成前台服务,核心就一个方法:setForeground()。这个方法在CoroutineWorker和Worker里都有,用法差不多。
我直接上代码,你感受一下:
class UploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 1. 创建通知
val notification = createNotification()
// 2. 设置前台信息
setForeground(
ForegroundInfo(
NOTIFICATION_ID,
notification
)
)
// 3. 执行实际任务
return try {
performUpload()
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
private fun createNotification(): Notification {
// 构建通知的代码...
}
}
嗯,这里要注意:setForeground()必须在doWork()里尽早调用。为什么?因为一旦调用了这个方法,系统就会把你的Worker绑定到一个前台服务上。如果你拖太久,系统可能已经把你的进程杀了。
我在项目中遇到过一个问题:有个同事在doWork()里先做了大量初始化,然后才调setForeground()。结果在低端机上,任务经常被系统回收。后来改成「先调setForeground,再做初始化」,问题就解决了。
创建ForegroundInfo:通知是门面
ForegroundInfo是前台服务的「门面」。它封装了通知ID和通知对象。你想想看,用户看到的就是这个通知,所以通知的设计很重要。
我一般会这样构建:
val notification = NotificationCompat.Builder(
applicationContext,
CHANNEL_ID
)
.setContentTitle("正在上传文件")
.setContentText("已上传: $progress%")
.setSmallIcon(R.drawable.ic_upload)
.setOngoing(true) // 不可滑动移除
.setProgress(100, progress, false)
.build()
val foregroundInfo = ForegroundInfo(
UPLOAD_NOTIFICATION_ID,
notification
)
这里有几个关键点:
- 通知渠道:Android 8.0+必须创建通知渠道,否则通知不显示
- setOngoing(true):让用户不能滑动移除,保证任务持续可见
- 进度条:如果任务有进度,用
setProgress()实时更新,用户体验会好很多
UPLOAD_NOTIFICATION_ID = 1001。这样方便管理,也避免和其他通知ID冲突。
前台服务的生命周期管理
前台服务不是「一劳永逸」的。它的生命周期需要你主动管理。说白了,什么时候开始,什么时候结束,都得心里有数。
WorkManager的前台服务生命周期大概是这样的:
- 任务开始:调用
setForeground(),系统启动前台服务 - 任务执行中:Worker持续运行,通知可以更新进度
- 任务结束:
doWork()返回Result,系统自动停止前台服务
但有个坑:如果你在doWork()里抛出了未捕获的异常,前台服务可能不会正常停止。我曾经踩过这个坑——任务崩溃了,通知栏还挂着「正在上传」的提示,用户一脸懵。
所以我的建议是:用try-catch包裹整个任务逻辑,确保无论成功还是失败,都能正确返回Result。
override suspend fun doWork(): Result {
return try {
setForeground(createForegroundInfo())
performTask()
Result.success()
} catch (e: Exception) {
Log.e("UploadWorker", "任务失败", e)
Result.failure()
}
}
Android 12+的前台服务限制
Android 12(API 31)开始,前台服务的限制变得非常严格。说白了,Google不想让App随便启动前台服务,尤其是用户不知情的情况下。
主要限制有这几条:
| 限制项 | 说明 |
|---|---|
| 前台服务启动限制 | 后台App不能随意启动前台服务(除非是特定场景) |
| 通知必须立即显示 | 前台服务启动后,通知必须在几秒内显示,否则系统会ANR |
| 新增前台服务类型 | 必须声明foregroundServiceType,比如dataSync、location等 |
| 权限要求 | 部分类型需要动态申请权限(比如FOREGROUND_SERVICE_LOCATION) |
那WorkManager怎么应对这些限制?好消息是,WorkManager已经帮我们处理了大部分兼容性问题。你只需要在Manifest里声明好前台服务类型:
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
然后在Worker里,用setForeground()时,WorkManager会自动处理前台服务的启动逻辑。但有个前提:你的App必须在前台,或者有合理的理由启动前台服务。
setForeground()可能会抛出ForegroundServiceStartNotAllowedException。这时候你需要考虑用WorkManager.enqueueUniqueWork()配合延迟执行,或者引导用户把App加入「省电白名单」。
我曾经在一个项目里,用户反馈「上传任务经常失败」。排查后发现,是Android 12的后台限制导致的。后来我们加了一个引导弹窗,让用户手动开启「允许后台运行」权限,问题才解决。
知识体系总览
下面这张图,帮你理清今天讲的核心逻辑:
好了,今天的内容就到这里。前台服务这块,说白了就是「用通知换保活」。但要注意,Google对前台服务的限制只会越来越严,所以能用WorkManager默认的Worker就别轻易上前台服务。只有在真正需要长时间运行的任务时,才考虑用setForeground()。
下一章我们会聊聊「WorkManager的观察者模式:如何监听任务状态」,到时候见。
公众号:蓝海资料掘金营,微信deep3321