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被用户划掉(或者长时间在后台),你想通过广播或者定时器启动前台服务,系统会直接拒绝。

豁免情况包括:

  • 用户可见的操作(比如点击通知、点击按钮)
  • 特定类型的服务(如mediaPlaybacklocationcamera等)
  • 系统白名单应用

来看一下如何在Manifest中声明前台服务类型:

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

重要提醒:从Android 14开始,前台服务类型的声明变得更加严格。如果你声明了foregroundServiceType,但实际使用中不符合该类型的行为,系统可能会抛出ForegroundServiceTypeNotAllowedException。我建议你在声明时仔细阅读官方文档,选择最匹配的类型。

13.5 知识体系总览

下面这张图总结了前台服务通知的核心流程和Android 12+的限制要点:

前台服务通知核心流程 1. 创建Service 2. 创建通知渠道 3. 构建通知 4. startForeground() 5. 更新通知(notify) 6. stopForeground() Android 12+ 前台服务限制 后台不能随意启动 (需用户可见操作) 通知延迟10秒显示 (系统优化) 必须声明服务类型 (foregroundServiceType) 10秒内必须调用 startForeground()

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