13、前台服务通知:Service.startForeground()、前台服务通知的强制性、前台服务通知的更新、Android 12+的前台服务限制
前台服务,说白了就是让用户「看得到」你在后台干活。我刚开始做Android开发时,总觉得这玩意儿就是个通知栏的装饰品。直到有一次,我的音乐App在后台播放时被系统直接杀掉,用户投诉电话打爆了产品经理的手机……嗯,从那以后,我才真正重视起前台服务。
13.1 什么是前台服务?为什么它是「强制性」的?
前台服务是一种特殊的Service。它必须有一个持续显示的通知,告诉用户:「嘿,我正在后台做事情,别杀我。」
为什么说是强制性的?因为从Android 8.0(API 26)开始,系统对后台服务做了严格限制。如果你想让Service在后台长时间运行,就必须把它变成前台服务。说白了,这是系统为了省电和提升用户体验,逼着你给用户一个交代。
核心要点:前台服务 = 普通Service + 持续通知。没有通知,就没有前台服务。
13.2 启动前台服务:startForeground() 的正确姿势
启动前台服务分两步走:先启动Service,再调用startForeground()。我见过不少新手把顺序搞反,结果通知死活不显示。
来看一个标准的音乐播放器前台服务示例:
public class MusicPlayerService extends Service {
private static final int NOTIFICATION_ID = 1001;
private static final String CHANNEL_ID = "music_player_channel";
@Override
public void onCreate() {
super.onCreate();
// 第一步:创建通知渠道(Android 8.0+ 必须)
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 第二步:构建通知
Notification notification = buildNotification();
// 第三步:启动前台服务
// 注意:必须在 startForeground() 之前确保通知渠道已创建
startForeground(NOTIFICATION_ID, notification);
// 开始播放音乐...
return START_STICKY;
}
private Notification buildNotification() {
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent,
PendingIntent.FLAG_IMMUTABLE
);
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在播放")
.setContentText("我的音乐 - 歌手名")
.setSmallIcon(R.drawable.ic_music_note)
.setContentIntent(pendingIntent)
.setOngoing(true) // 不可滑动清除
.build();
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"音乐播放",
NotificationManager.IMPORTANCE_LOW // 前台服务通常用 LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}
}
个人经验:我习惯把NOTIFICATION_ID定义成常量,方便后续更新通知时使用。另外,PendingIntent.FLAG_IMMUTABLE是Android 12+的推荐做法,能避免一些安全漏洞。
13.3 前台服务通知的更新:不只是显示一次
前台服务的通知不是一成不变的。比如音乐播放器,你得显示当前歌曲名、播放进度。这就涉及到通知的更新。
更新通知其实很简单:重新构建一个Notification对象,然后调用NotificationManager.notify(),使用相同的NOTIFICATION_ID即可。
// 更新通知示例:切换歌曲时
private void updateNotification(String songName, String artist) {
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在播放")
.setContentText(songName + " - " + artist)
.setSmallIcon(R.drawable.ic_music_note)
.setOngoing(true)
.build();
NotificationManager manager = getSystemService(NotificationManager.class);
manager.notify(NOTIFICATION_ID, notification);
}
这里有个坑:千万不要在更新通知时重新调用startForeground()。我见过有人这么干,结果每次更新都会触发前台服务的重新绑定,导致奇怪的ANR问题。正确的做法是直接用NotificationManager.notify()更新。
我曾经踩过的坑:有一次我在更新通知时忘了传相同的NOTIFICATION_ID,结果系统创建了一个新的通知,旧的通知还挂在通知栏上。用户看到两个「正在播放」,体验极差。记住:更新通知必须用同一个ID。
13.4 Android 12+ 的前台服务限制:越来越严了
Android 12(API 31)对前台服务做了更严格的限制。说白了,Google觉得开发者滥用前台服务的情况太多了,得管管。
| 限制项 | 说明 | 影响范围 |
|---|---|---|
| 前台服务启动限制 | 后台应用不能随意启动前台服务(除非特定豁免) | targetSdkVersion 31+ |
| 通知延迟显示 | 前台服务启动后,通知可能延迟10秒显示 | 所有应用 |
| 前台服务类型声明 | 必须在Manifest中声明foregroundServiceType |
targetSdkVersion 31+ |
| 短时间任务限制 | 10秒内未调用startForeground()会抛出异常 |
所有应用 |
其中最要命的是第一条:后台应用不能随意启动前台服务。什么意思呢?如果你的App被用户划掉(或者长时间在后台),你想通过广播或者定时器启动前台服务,系统会直接拒绝。
豁免情况包括:
- 用户可见的操作(比如点击通知、点击按钮)
- 特定类型的服务(如
mediaPlayback、location、camera等) - 系统白名单应用
来看一下如何在Manifest中声明前台服务类型:
<service
android:name=".MusicPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
重要提醒:从Android 14开始,前台服务类型的声明变得更加严格。如果你声明了foregroundServiceType,但实际使用中不符合该类型的行为,系统可能会抛出ForegroundServiceTypeNotAllowedException。我建议你在声明时仔细阅读官方文档,选择最匹配的类型。
13.5 知识体系总览
下面这张图总结了前台服务通知的核心流程和Android 12+的限制要点:
13.6 避坑指南:我踩过的那些坑
做前台服务这么多年,我总结了几条血泪教训:
- 不要在
onCreate()里调用startForeground():这时候Service还没完全初始化,调用会失败。应该在onStartCommand()里调用。 - 别忘了处理
stopForeground():当你的前台任务结束时,记得调用stopForeground(true)来移除通知。参数true表示同时移除通知。 - Android 12+ 的
PendingIntent必须指定可变性:要么FLAG_IMMUTABLE,要么FLAG_MUTABLE。不指定的话,系统会报错。 - 前台服务类型别乱填:比如你做个下载服务,却声明成
mediaPlayback,系统检测到行为不匹配,可能会在Android 14上直接崩溃。
我的个人习惯:我会在onStartCommand()里加一个计时器,如果5秒内startForeground()还没调用成功,就主动记录日志并降级处理。这样能避免Android 12+的10秒限制导致的崩溃。
前台服务通知这块,说白了就是系统在「用户体验」和「后台能力」之间找平衡。你越理解这些限制背后的设计意图,写出来的代码就越不容易踩坑。嗯,今天就先聊到这里,下次我们聊聊前台服务的进阶用法——如何优雅地处理多任务场景。
公众号:蓝海资料掘金营,微信deep3321