第一章 前台服务基础:定义、生命周期、启动方式
大伙儿都知道,Android 里 Service 分两种:后台服务和前台服务。但你真的清楚前台服务到底强在哪?说白了,前台服务就是「戴了个高帽子的服务」——它通过一个持续存在的通知,告诉系统「我很重要,别杀我」。嗯,这个机制在 API 26 以后尤其关键。
1.1 前台服务的定义
官方文档说它「用于执行用户可感知的长时间运行操作」。我个人习惯把它理解成:用户肉眼能看到的后台任务。举个例子,音乐播放器、导航 App、运动跟踪——这些场景如果只用普通 Service,系统很可能在你切到后台几分钟后就把它杀了。前台服务就不一样,只要通知还在,系统就认为用户知道你在干活,不会轻易回收。
我记得第一次给项目加前台服务时,同事说「不就是加个通知嘛」。结果踩了坑——我忘了在 Android 12 以上还需要 FOREGROUND_SERVICE_TYPE,导致高版本直接崩溃。当时那个崩溃日志我到现在还记得清清楚楚。
Activity.startForegroundService() 或者 AlarmManager 等合法途径。否则你会收到 ForegroundServiceStartNotAllowedException。
1.2 生命周期:比想象中多几步
前台服务的生命周期比普通 Service 多了两个关键节点:创建通知、调用 startForeground()。整体流程如下:
- 创建服务实例: 系统调
onCreate() - 处理启动请求:
onStartCommand()被调用,拿到 Intent - 绑定通知: 必须在
onStartCommand()返回前调用startForeground(),传递 notification ID 和 Notification 对象 - 运行中: 服务保持存活,系统不会轻易杀掉
- 解除前台状态: 调用
stopForeground()(可选),或所有工作完成后调用stopSelf() - 销毁:
onDestroy()清理资源
这里有个容易翻车的地方——从 Android 12(API 31)开始,你必须在 startForeground() 之前声明 android:foregroundServiceType 属性,并且要在 manifest 里配置正确的类型。我去年帮一个同事排查 bug,他死活不记得加这个声明,结果每次在 onStartCommand 里 startForeground() 就 crash。查了半小时才发现。
生命周期时间线(常见场景):
| 阶段 | 系统行为 | 开发者注意事项 |
|---|---|---|
| onCreate | 初始化资源 | 避免耗时操作 |
| onStartCommand | 接收启动Intent | 5秒内调用startForeground() |
| 运行中 | 系统维持进程高优先级 | 及时更新通知内容 |
| onDestroy | 清理回收 | 释放后台线程、Closeable资源 |
1.3 启动方式:三种姿势,别搞混
启动前台服务主要有三种方式,每种都有它的适用场景。我按 API 级别给你捋一捋:
- 方式一:startService(API 26 以前)
老项目常见,直接startService(intent),然后在onStartCommand里调startForeground()。但现在千万别这么写 —— 高版本会直接抛异常。 - 方式二:startForegroundService(API 26+ 推荐)
标准写法:context.startForegroundService(intent)。注意,必须在onStartCommand返回前调startForeground(),不然系统会看它超时,直接 kill 掉服务并报Context.startForegroundService() did not then call Service.startForeground()。 - 方式三:通过 JobScheduler / AlarmManager 间接启动
适用于需要精确定时或者满足某个条件再启动的场景。比如我用AlarmManager设置一个PendingIntent指向BroadcastReceiver,在onReceive里再调startForegroundService。但一定要注意:Android 12 以后,AlarmManager调起的定时任务如果延迟了,前台服务启动时间窗口可能已经过期。
你可能会问:「为什么不能直接在 onCreate 里调 startForeground?」嗯,onCreate 时还没有 Intent,你连 task ID 都拿不到。系统要求在 onStartCommand 收到启动 Intent 后才能绑定通知,这是有道理的——通知内容通常要基于 Intent 传过来的数据。
下面是一个标准的前台服务启动代码模板,我用在了好几个项目里,稳定运行没出过问题:
// 在 Activity 或 Fragment 中启动
Intent intent = new Intent(this, MyForegroundService.class);
intent.putExtra("action", "start");
ContextCompat.startForegroundService(this, intent);
// MyForegroundService.java
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent != null ? intent.getStringExtra("action") : "";
if ("start".equals(action)) {
// 构建通知(省略具体代码)
Notification notification = buildNotification();
// 必须在5秒内调用
startForeground(NOTIFICATION_ID, notification);
// 开始实际工作...
doWork();
}
return START_STICKY; // 或者 START_REDELIVER_INTENT
}
FOREGROUND_SERVICE,你还需要在 manifest 里指定具体的 foregroundServiceType,比如 dataSync、location、mediaPlayback 等等。如果不匹配,startForeground() 会直接挂。这个属于系统级限制,绕不过去,老老实实声明就好。
1.4 知识框架:一张图理清
下面这张图概括了前台服务的核心要素:启动方式、生命周期节点、以及和普通 Service 的区别。我画的时候特意把「通知绑定」这一步标了红色——这是最容易出 bug 的地方。
1.5 一些实战经验
我有个项目是做运动追踪的,用前台服务记录 GPS 轨迹。开始没注意,在 Android 12 设备上测试,每次切后台不到一分钟服务就被杀了。找了好久才发现——我没有在 manifest 里声明 android:foregroundServiceType="location"。加上就好了。你想想看,一个前台服务如果连类型都不声明,系统怎么知道你该有什么权限?
另外,关于 START_STICKY 和 START_REDELIVER_INTENT 的选择,我个人习惯:如果服务是「轮训」式的(比如每30秒检查一次服务器),用 START_STICKY,被杀后会重启但不重传 Intent;如果是「一次性任务」且有重要数据(比如上传文件),用 START_REDELIVER_INTENT,被杀后系统会重新给你那个 Intent。这个选择会影响前台服务异常后的行为,我曾经因为用了 START_NOT_STICKY,结果用户刚打开 App 就切后台,服务直接不跑了。
adb shell dumpsys activity services 查看当前所有正在运行的 Service,包括前台状态、进程优先级、是否绑定通知等。我一般在测试 crash 后都会跑一下这个命令,看看服务是不是还活着。