Foreground Service 详解:启动方式、通知、优先级
前台服务这个概念,我在面试时经常被问到,实际上手项目时也踩过不少坑。简单说,前台服务就是一个用户能「明确感知到」的后台任务——它必须带一个通知栏提醒,并且优先级比普通后台服务高得多。系统不会轻易杀掉它,除非内存真扛不住了。
咱们从三个核心维度拆开看:怎么启动它、通知怎么写才合规、以及它的优先级到底意味着什么。
启动方式:两种途径,一个目的
Android 里启动前台服务有两种写法,但底层逻辑是一样的。
关键区别:Android 8.0(API 26)之后,系统强制要求后台服务必须在 5 秒内调用 startForeground(),否则直接报 ForegroundServiceDidNotStartInTimeException。我自己就遇到过线上崩溃——当时没注意通知 channel 的配置,结果服务一直没调 startForeground,用户直接闪退。嗯,很痛。
写法一:使用 startForegroundService()
// 在组件中启动
Intent intent = new Intent(this, MyForegroundService.class);
startForegroundService(intent);
写法二:使用 startService()(仅限 Android 8.0 以下)
Intent intent = new Intent(this, MyForegroundService.class);
startService(intent);
你想想看,现代开发基本都只面向 API 26+ 了,所以 startForegroundService() 是唯一推荐方式。服务端的 onStartCommand 里必须做两件事:
- 创建并配置通知 channel(Android 8.0+ 必须)
- 调用
startForeground(notificationId, notification)
代码骨架长这样:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 1. 创建通知 channel
NotificationChannel channel = new NotificationChannel(
"foreground_channel",
"前台服务",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.createNotificationChannel(channel);
// 2. 构建通知
Notification notification = new Notification.Builder(this, "foreground_channel")
.setContentTitle("运行中")
.setContentText("服务正在后台工作")
.setSmallIcon(R.drawable.ic_service)
.build();
// 3. 启动前台服务
startForeground(1, notification);
return START_STICKY;
}
我个人习惯把 notification channel 的 importance 设为 IMPORTANCE_LOW 或 IMPORTANCE_MIN,这样不会产生声音打扰用户,又能保证通知可见。地图导航类 app 才用 IMPORTANCE_HIGH。
通知的核心:channel、id、icon 缺一不可
前台服务的通知可不是你想怎么写就怎么写。系统很严格,尤其 Android 12(API 31)之后加了不少限制。
通知 channel 必须唯一且固定:同一个前台服务通知 channel 只能有一个。我见过有人每次启动都新建 channel,结果通知反复闪烁——那是不行的。
通知 ID 需要唯一:同一个 app 内多个前台服务不能共用一个 notification ID,否则后启动的会覆盖前一个。如果你同时做音乐播放和文件下载,两个服务就要分配不同的 ID。
小图标必须存在:通知必须设置 setSmallIcon(),否则部分 ROM 会直接忽略前台服务请求。我曾经碰到华为手机上通知不显示,查了半天发现是 icon 资源被混淆了……
| 元素 | 必须 / 可选 | 说明 |
|---|---|---|
| 通知 channel | 必须(API 26+) | 需先创建再使用,importance 推荐 LOW |
| Notification ID | 必须 | 每个前台服务唯一,建议用常量定义 |
| 小图标 | 必须 | 建议 24dp 白色图标,适配暗色模式 |
| 标题+内容 | 强烈建议 | 用户需要知道服务在做什么 |
我曾经遇到一个坑:在 Android 12 上,前台服务通知必须让用户能一键关闭或操作(比如停止按钮),否则系统会认为服务「不可关闭」而拒绝运行。后来我加了一个 .addAction(停止按钮) 才通过 Google Play 审核。
优先级:为什么前台服务杀不掉?
前台服务的优先级对应的是 android.app.Service.START_STICKY 的返回值和系统进程调度权重。说白了,系统在杀进程时有一个「白名单」:前台服务进程的 oom_adj 值很低,大概在 -11 到 -16 之间(普通后台服务是 0 到 10)。数值越小表示越不容易被杀死。
我们来对比一下:
| 服务类型 | oom_adj 范围 | 被杀概率 | 典型场景 |
|---|---|---|---|
| 前台服务 | -11 ~ -16 | 极低(仅内存不足时) | 音乐播放、位置跟踪、文件下载 |
| 普通后台服务 | 0 ~ 10 | 高(Android 8+ 还会被限制) | 定时同步、后台计算 |
| 绑定服务(已绑定 Activity) | 取决于绑定进程 | 中等 | IPC、音乐控制 |
为什么 Google 要这么设计?因为前台服务是用户「正在使用」的功能的载体。比如你正在导航,系统如果把导航服务杀了,你半路没声音了,这体验就彻底崩了。所以优先级必须高。
不过注意一点:高优先级不是免死金牌。如果系统内存压力极大,还是会优先杀普通后台进程,但前台服务的进程通常至少能留到最后。嗯,这里敲个重点:如果你的 app 中多个前台服务都在同一个进程里,一个服务被杀会导致整个进程挂掉,所以要考虑把高关键任务放在单独进程里。
归纳总结:核心知识图谱
下面这张图帮你把 Foreground Service 的核心概念串起来。我建议你把这张图存在手机里,面试前翻一翻。
从图里你能看到三个分支:启动方式、通知核心、优先级。它们互相独立但又共同决定了前台服务的生命周期。
个人经验总结:搞前台服务最容易翻车的三个点——通知 channel 忘记创建、startForeground() 没在 5 秒内调用、多个前台服务共用同一个 notification ID。这三点你在测试阶段一定要覆盖到。
好了,这一章咱们把前台服务的启动、通知和优先级掰开揉碎了讲清楚了。接下来的实战部分,我会带你用 Mock 回放的方式验证这些知识——你会看到真实的 crash 和解决方案。