1. 前台服务概述

说实话,做 Android 开发这么久,每次遇到前台服务,我心里都会多留个心眼。它太容易被滥用了,又太容易被忽略细节。很多人觉得——不就是加个通知嘛?其实远不止这么简单。

这一章,我们先打好地基。把前台服务是什么、怎么启动、怎么销毁,彻底搞明白。后面聊 CPU 抖动的问题,你才会有感觉。

什么是前台服务?

前台服务,说白了,就是一种用户能“感知到”的后台任务。系统会用一条通知栏消息,明明白白告诉用户:你的 App 正在干某件事。

嗯,这里要注意:

  • 它本质上还是 Service,四大组件之一。
  • 但它比普通 Service 更“不容易被系统杀死”。
  • 它强制要求一个可见的通知,不能偷偷摸摸跑。
核心区别:普通 Service 是后台任务,系统可以随时回收;前台服务是用户能看到的任务,系统会尽可能保活。

我当初刚入行时,就踩过一次坑。当时做了一个音乐播放器,觉得后台播放没必要加通知。结果 Android 8.0 一上,播放器跑几分钟就被系统干掉了。用户骂翻了评论区。从那以后,我对前台服务的“通知可见性”再也不敢马虎。

启动方式

启动前台服务,其实就三步。但三步里每一步都有坑。

第一步:在 AndroidManifest 里声明

<service
    android:name=".MyForegroundService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false" />

你想想看,很多人会忘记 foregroundServiceType。Android 10 之后,这个字段是强制的。不写,系统会给你弹一个 ForegroundServiceTypeNotAllowedException。

第二步:启动服务

val intent = Intent(this, MyForegroundService::class.java)
ContextCompat.startForegroundService(this, intent)
警告:从 Android 8.0 开始,startService() 已经不能直接启动前台服务了。必须用 startForegroundService()。否则系统直接当成后台 Service 处理。

第三步:在 Service 内调用 startForeground()

class MyForegroundService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = createNotification()
        // 关键:5 秒内必须调用,否则系统会 ANR 并杀死服务
        startForeground(NOTIFICATION_ID, notification)
        return START_STICKY
    }
}

这里有一个隐藏陷阱。我曾经就遇到过一个同事,在 onCreate() 里调用了 startForeground(),结果服务还没完全初始化,通知构建失败了。5 秒窗口期一过,系统直接抛了一个 Context.startForegroundService() did not then call Service.startForeground() 的异常。很尴尬。

我个人习惯的做法是:在 onStartCommand() 里先做好所有初始化,最后调用 startForeground(),确保万无一失。

生命周期

前台服务的生命周期,比普通 Service 多了一个“通知绑定”的环节。整个流程是这样的:

回调 说明 常见坑点
onCreate() 服务创建,初始化资源 不要在这里调用 startForeground()
onStartCommand() 接收启动请求,调用 startForeground() 5 秒窗口期,超时不调用就 ANR
服务运行中 持续执行任务,通知可见 留意 CPU 占用,会引发 Compose 重组抖动
onDestroy() 停止前台,移除通知,释放资源 记得调用 stopForeground() 和 stopSelf()

关于 onStartCommand() 的返回值,我建议你用 START_STICKY。为什么呢?

  • START_STICKY:系统杀死服务后会重新创建,但会传一个 null Intent。适合需要持续运行的任务。
  • START_NOT_STICKY:系统杀死后不再重启。适合一次性任务。
  • START_REDELIVER_INTENT:会重新传递上次的 Intent。适合需要完整恢复的任务。

我个人习惯是:如果你不确定,用 START_STICKY 是最稳妥的。但要注意——重新创建时的 null Intent 处理,很多人会忽略。

技巧:前台服务不是用来保活的!不要把播放器、定位、下载等任务强行塞给前台服务来对抗系统杀进程。Android 系统在内存不足时,依然会杀死前台服务——只是优先级比普通 Service 高一些。我曾经帮一个电商 App 做过优化,他们把整个 SDK 初始化都挂在前台服务上,结果一杀全崩。说白了,前台服务的设计初衷是“用户可见的任务”,不是“你的 App 保镖”。

最后,做一张图把前台服务的启动流程和生命周期的核心脉络串起来,方便你记忆。

前台服务启动与生命周期流程 1. 在Manifest声明 · 声明Service组件 2. startForegroundService() · 必须用Android 8.0+ API 3. onCreate() · 初始化资源 4. onStartCommand() · 5秒内调用startForeground() · 选择返回:START_STICKY等 5. startForeground() · 绑定通知到服务 6. 服务运行中 · 执行任务·CPU/IO工作 7. onDestroy() · stopForeground() ⚠️ 关键提醒 1. 不要在前台服务里做重型CPU任务 2. 通知必须及时更新 3. 停止时及时清理资源

流程图看下来,你会发现核心就两个:通知绑定和生命周期回调。但就是这两个看似简单的东西,在 CPU 占用高的时候,会给 Compose 的重组机制带来肉眼可见的卡顿。下一章,我们会深入分析这个问题。

接下来,就是实战的部分了。