2. 前台服务与普通Service的区别

很多同学刚接触Android Service时,总觉得前台服务和普通Service差不多,无非是多加个通知栏。其实不然。我个人的经验是:搞不清这个区别,线上崩溃率至少能高出两个点。说白了,前台服务是向系统交了“保护费”的Service,普通Service则是随时可能被砍掉的“临时工”。咱们今天就把这两个东西彻底掰开揉碎。

2.1 它们到底差在哪?

先问一个问题:系统为什么要把Service分成前台和普通两类?核心原因只有一个——内存回收策略不同。

普通Service运行在后台,当系统内存紧张时,会优先杀死后台进程。你想想看,如果你的音乐App用普通Service播放,来电时系统一回收,歌就断了。这体验能忍?所以Google搞出了前台Service,它通过一个常驻通知告诉用户:“这个App正在干活,别杀我。”系统看到前台Service,会把进程的优先级提到和用户可见的Activity差不多,不到万不得已绝不回收。

为了让大家更直观地理解,我画了一张对比图:

前台服务 vs 普通Service——核心区别 普通 Service ❌ 无前台通知 ⬇️ 进程优先级:低 ☠️ 易被系统杀死(Level 40+) 🔒 后台执行限制严格(API 26+) 📱 不占用前台资源 前台 Service ✅ 必须显示前台通知 ⬆️ 进程优先级:高(几乎等同Activity) 🛡️ 很难被系统杀死(Level 5+) ♻️ 后台限制豁免(需声明前台类型) ⚡ 占用前台资源(CPU/内存) 通知差异 优先级 杀死风险 核心差异:前台Service通过通知换取高优先级, 且必须自Android 14起声明前台服务类型。

2.2 代码实现上的差异

普通Service启动很简单,startService一下就行了。前台Service则需要跨三步:创建Notification、调用startForeground、在AndroidManifest中声明foregroundServiceType。

我拿一个实时位置上报的场景举例。以下代码演示了如何实现一个前台Service:

public class LocationForegroundService extends Service {
    private static final String CHANNEL_ID = "location_channel";
    private static final int NOTIFY_ID = 1001;

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 构建通知 —— 这是硬性要求
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("正在获取位置")
                .setContentText("为了实时导航,请勿关闭通知")
                .setSmallIcon(R.drawable.ic_location)
                .setOngoing(true)
                .build();

        startForeground(NOTIFY_ID, notification);
        // 这里开始执行位置监听等耗时任务
        startLocationUpdates();
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        stopForeground(true);  // 移除通知
        stopLocationUpdates();
        super.onDestroy();
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID, "位置服务", NotificationManager.IMPORTANCE_LOW);
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
    }
}

我个人的习惯:在startForeground之后,我会立刻保存一个全局变量记录当前Service已经是前台状态。下次再调用startService时,可以复用已有的通知,避免重复创建导致ANR。我在项目中遇到过不止一次因为重复startForeground而闪退的bug。

2.3 生命周期与用户感知的差异

普通Service的生命周期完全由代码控制,用户基本感知不到。而前台Service会一直显示一个通知,用户可以通过滑动关闭通知来停止服务(但需要额外处理)。嗯,这里要注意:Android 13开始,用户可以手动移除前台通知,此时系统会回调onForegroundServiceNotificationDismissed,你必须在这里停止Service,否则系统会直接杀掉进程。

我在一个打车项目里就踩过这个坑。用户划掉了通知,但我们的定位Service还在偷偷跑,结果被Google Play判定为违规,差点下架。后来老老实实加了监听:

@Override
public void onForegroundServiceNotificationDismissed() {
    stopForeground(true);
    stopSelf();
}

2.4 前台服务类型声明(Android 14强制)

从Android 14(API 34)开始,Google要求所有前台Service必须在Manifest中明确声明其类型,而且类型必须对应实际用途。举个例子,你这个Service用来做位置上报,就必须声明location类型;如果是播音乐,就声明mediaPlayback。乱写类型?直接运行时崩溃。

下面这张表帮你快速对照:

前台服务类型 XML声明示例 典型用途
location android:foregroundServiceType="location" 导航、实时位置追踪
mediaPlayback android:foregroundServiceType="mediaPlayback" 音乐播放、播客
camera android:foregroundServiceType="camera" 后台录像、扫码
connectedDevice android:foregroundServiceType="connectedDevice" 蓝牙、USB设备通信
dataSync android:foregroundServiceType="dataSync" 文件上传/下载

曾经有一位同事,把定位Service声明成了dataSync,结果审核被拒。您千万别犯这种低级错误。声明类型必须和Service的onStartCommand中实际执行的任务一致。

2.5 什么时候选前台,什么时候选普通?

我总结了一个“三问判断法”:

  1. 用户能直观感知到服务运行吗? 比如音乐播放、通话录音、导航 —— 选前台。
  2. 服务被杀死后会有严重后果吗? 比如正在上传医疗文件、录制重要会议 —— 选前台。
  3. 系统会不会因为后台限制而中断? Android 12+对后台启动Service限制极严 —— 如果任务无法通过WorkManager替代,那就必须前台。

如果以上三点全是“否”,那你用普通Service+JobScheduler或者WorkManager会更省电,也免去了通知打扰。我个人更倾向于:能不用前台就不用前台,毕竟通知栏不干净也影响用户体验。但一旦需要,就必须用得规范。

2.6 避坑指南(全是亲身经历)

  • 忘记调用stopForeground:有一次我测试时直接调了stopSelf,结果Service停了但通知还在,用户非得手动点进应用才能关闭。应该先调stopForeground(true)再stopSelf。
  • 后台Service突然变前台导致ANR:如果在后台调用了startForeground但没在5秒内发出通知,系统会报ANR。解决方案:先保证通知渠道和Builder准备好再调用。
  • Android 12上的通知点击跳转:前台Service的通知必须能点击跳转,否则会被系统标记为“不良通知”。我习惯加一个PendingIntent指向主界面的特定页面。
  • 进程被杀后自动恢复:前台Service在进程被系统杀死后,默认会重启(START_STICKY)。如果你不希望自动重启,可以用START_NOT_STICKY。

一句话总结:前台Service用通知换优先级,适合用户能感知的持续任务;普通Service适合不可见的后台任务,但随时可能被系统回收。把两者区分清楚,你的App在资源竞争中的存活率会高很多。