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 里必须做两件事:

  1. 创建并配置通知 channel(Android 8.0+ 必须)
  2. 调用 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 的核心概念串起来。我建议你把这张图存在手机里,面试前翻一翻。

Foreground Service 核心知识结构 前台服务 启动方式 通知核心 优先级 startForegroundService() onStartCommand 内调 startForeground() 通知 channel(唯一) 小图标 + 唯一 ID Android 12 需可操作按钮 oom_adj = -11 ~ -16 被杀概率远低于后台服务 建议关键任务放单独进程 ⚠️ 核心要求 启动后5秒内必须 startForeground() | 通知必须可见 | Android 12+ 注意可关闭性限制

从图里你能看到三个分支:启动方式、通知核心、优先级。它们互相独立但又共同决定了前台服务的生命周期。

个人经验总结:搞前台服务最容易翻车的三个点——通知 channel 忘记创建、startForeground() 没在 5 秒内调用、多个前台服务共用同一个 notification ID。这三点你在测试阶段一定要覆盖到。

好了,这一章咱们把前台服务的启动、通知和优先级掰开揉碎了讲清楚了。接下来的实战部分,我会带你用 Mock 回放的方式验证这些知识——你会看到真实的 crash 和解决方案。