21、WorkManager的前台服务:setForeground()方法、创建ForegroundInfo、前台服务的生命周期管理、Android 12+的前台服务限制

说实话,后台任务调度这块,最让人头疼的就是「保活」问题。你想想看,用户把App切到后台,系统随时可能把你的任务给杀了。尤其是Android 12之后,限制越来越严,光靠WorkManager的默认Worker,有时候真不够用。

那怎么办?前台服务就是答案。说白了,就是给你的后台任务「加个护身符」——让系统知道,这个任务很重要,用户也能看到通知栏里有个提示。

我个人习惯,凡是需要长时间运行、用户能感知到的任务(比如文件上传、数据同步),都会考虑用前台服务。今天我们就来聊聊,WorkManager里怎么玩转前台服务。

setForeground()方法:让Worker「前台化」

在WorkManager里,想让一个Worker变成前台服务,核心就一个方法:setForeground()。这个方法在CoroutineWorkerWorker里都有,用法差不多。

我直接上代码,你感受一下:

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()实时更新,用户体验会好很多
小技巧:我习惯把通知ID定义成常量,比如UPLOAD_NOTIFICATION_ID = 1001。这样方便管理,也避免和其他通知ID冲突。

前台服务的生命周期管理

前台服务不是「一劳永逸」的。它的生命周期需要你主动管理。说白了,什么时候开始,什么时候结束,都得心里有数。

WorkManager的前台服务生命周期大概是这样的:

  1. 任务开始:调用setForeground(),系统启动前台服务
  2. 任务执行中:Worker持续运行,通知可以更新进度
  3. 任务结束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,比如dataSynclocation
权限要求 部分类型需要动态申请权限(比如FOREGROUND_SERVICE_LOCATION

那WorkManager怎么应对这些限制?好消息是,WorkManager已经帮我们处理了大部分兼容性问题。你只需要在Manifest里声明好前台服务类型:

<service
    android:name="androidx.work.impl.foreground.SystemForegroundService"
    android:foregroundServiceType="dataSync"
    android:exported="false" />

然后在Worker里,用setForeground()时,WorkManager会自动处理前台服务的启动逻辑。但有个前提:你的App必须在前台,或者有合理的理由启动前台服务

注意:Android 12+上,如果你的App在后台,WorkManager的setForeground()可能会抛出ForegroundServiceStartNotAllowedException。这时候你需要考虑用WorkManager.enqueueUniqueWork()配合延迟执行,或者引导用户把App加入「省电白名单」。

我曾经在一个项目里,用户反馈「上传任务经常失败」。排查后发现,是Android 12的后台限制导致的。后来我们加了一个引导弹窗,让用户手动开启「允许后台运行」权限,问题才解决。

知识体系总览

下面这张图,帮你理清今天讲的核心逻辑:

WorkManager前台服务知识体系 setForeground() ForegroundInfo 通知ID + Notification对象 生命周期管理 开始 → 执行 → 结束 Android 12+ 限制 后台启动限制 / 通知ANR 核心要点 • setForeground() 必须在 doWork() 尽早调用 • 通知必须包含进度信息,提升用户体验 • Android 12+ 需声明 foregroundServiceType,注意后台启动限制

好了,今天的内容就到这里。前台服务这块,说白了就是「用通知换保活」。但要注意,Google对前台服务的限制只会越来越严,所以能用WorkManager默认的Worker就别轻易上前台服务。只有在真正需要长时间运行的任务时,才考虑用setForeground()

下一章我们会聊聊「WorkManager的观察者模式:如何监听任务状态」,到时候见。


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