2. 前台服务与普通Service的区别
很多同学刚接触Android Service时,总觉得前台服务和普通Service差不多,无非是多加个通知栏。其实不然。我个人的经验是:搞不清这个区别,线上崩溃率至少能高出两个点。说白了,前台服务是向系统交了“保护费”的Service,普通Service则是随时可能被砍掉的“临时工”。咱们今天就把这两个东西彻底掰开揉碎。
2.1 它们到底差在哪?
先问一个问题:系统为什么要把Service分成前台和普通两类?核心原因只有一个——内存回收策略不同。
普通Service运行在后台,当系统内存紧张时,会优先杀死后台进程。你想想看,如果你的音乐App用普通Service播放,来电时系统一回收,歌就断了。这体验能忍?所以Google搞出了前台Service,它通过一个常驻通知告诉用户:“这个App正在干活,别杀我。”系统看到前台Service,会把进程的优先级提到和用户可见的Activity差不多,不到万不得已绝不回收。
为了让大家更直观地理解,我画了一张对比图:
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 什么时候选前台,什么时候选普通?
我总结了一个“三问判断法”:
- 用户能直观感知到服务运行吗? 比如音乐播放、通话录音、导航 —— 选前台。
- 服务被杀死后会有严重后果吗? 比如正在上传医疗文件、录制重要会议 —— 选前台。
- 系统会不会因为后台限制而中断? 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在资源竞争中的存活率会高很多。