前台服务生命周期详解:onCreate、onStartCommand、onDestroy 回调顺序与注意事项
前台服务的生命周期,说白了就是三个核心回调:onCreate、onStartCommand、onDestroy。很多刚接触 Service 的开发者容易混淆它们的调用时机,或者认为和 Activity 类似。嗯,这里我要提醒一句:**Service 的生命周期要简单得多,但坑往往藏在细节里**。我个人习惯在写每个前台服务前,先把这三个回调的时序图画在纸上,再动手编码。
核心认知:前台服务的生命周期不依赖于 Activity 或 Fragment,它由系统进程管理。一旦启动,即使 App 切到后台,服务仍在运行(除非被系统杀死)。
1. 回调执行顺序
你想想看,一个前台服务从出生到销毁,走的是这样的流程:
onCreate()– 仅一次:第一次通过startService()或startForegroundService()启动服务时调用。如果服务已经创建过,即使多次 start,onCreate也只走一次。onStartCommand()– 每次 start 都调用:每次有人调用startService()或startForegroundService()都会触发。注意,系统可能因资源紧张杀掉服务后重启,重新调用onStartCommand并传入上一次的 Intent。onDestroy()– 仅一次:当stopService()或stopSelf()调用后,服务结束生命周期。如果服务被系统强制杀死,onDestroy可能不执行(嗯,这里是个大坑)。
我曾在项目中遇到过一个问题:重复调用 startForegroundService() 导致 onStartCommand 执行多次,而 onCreate 只执行一次。我在 onCreate 里初始化了一个定时器,结果定时器只被创建一次,后续 start 拿到的 Intent 却不被处理——因为我把启动逻辑写在 onCreate 里了。**经验教训:启动逻辑要放在 onStartCommand 中,onCreate 只做一次性资源初始化。**
警告:千万不要在 onCreate 里做耗时操作(超过几毫秒),因为前台服务的主线程就是 UI 线程(除非你单独开线程)。我见过有人直接在 onCreate 里做网络请求,结果 ANR。前台服务虽然优先级高,但依然受主线程超时限制。
2. onStartCommand 返回值详解
这个回调的返回值直接影响服务被系统杀死后的重启行为。我当初做第一个前台服务时,直接写了默认的 START_STICKY,结果服务被杀后不断自动重启,导致 CPU spike。
| 返回值 | 含义 | 重启行为 |
|---|---|---|
START_NOT_STICKY |
系统杀掉服务后,不重启 | 适合只处理一次性任务的场景(如网络请求) |
START_STICKY |
系统杀掉服务后,会自动重启(但 Intent 为 null) | 适合需要常驻后台的服务(如音乐播放),但重启时拿不到上次的 Intent |
START_REDELIVER_INTENT |
系统杀掉服务后,会重启并重新传递上次的 Intent | 适合任务必须完成的场景(如文件上传),但要注意 Intent 可能堆积 |
个人经验建议:如果服务只是临时做一件后台任务(比如下载一张图片),用 START_NOT_STICKY 或 START_REDELIVER_INTENT。我踩过最深的坑是用了 START_STICKY 做上传服务,结果用户取消任务后服务又自己重启了,白白消耗用户流量。
3. 前台通知与生命周期的纠缠
前台服务还有一个隐藏要求:在 onStartCommand 调用后 5 秒内必须调用 startForeground() 并发布通知。否则系统会抛出 ForegroundServiceDidNotStartInTimeException 并杀死服务。我记得 Android 12 开始这个限制更加严格。
我的习惯写法:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 第一时间调用 startForeground
Notification notification = buildNotification();
startForeground(NOTIFICATION_ID, notification);
// 然后处理业务逻辑
handleTask(intent);
return START_STICKY;
}
注意:不要在 onCreate 里提前调用 startForeground(),因为那时系统可能还没完全建立起前台服务的上下文。我曾经在 onCreate 里提前发通知,结果在 Android 8.0 上崩溃。确保在 onStartCommand 中调用,且必须在 super.onStartCommand() 之前(或尽早)。
4. onDestroy 中的清理工作
这个回调是最后的机会释放资源。后台线程、定时器、广播接收器、文件句柄等都要在这里清理。但是,别忘了调用 stopForeground(true),否则前台通知会一直留在状态栏,除非 App 被彻底杀死。
我见过一个很典型的 bug:onDestroy 里只停了线程,没移除通知。用户关闭服务后,看到通知还在,以为 App 有流氓行为。嗯,这里要养成好习惯:
@Override
public void onDestroy() {
super.onDestroy();
// 移除前台通知
stopForeground(true);
// 释放资源
if (executor != null) {
executor.shutdownNow();
}
unregisterReceiver(myReceiver);
}
5. 一个真实的踩坑经历
我曾经做一个 GPS 轨迹记录 App,前台服务需要持续上传位置。一开始我把上传逻辑写在 onCreate 里,想着只初始化一次。结果用户反馈:退出 App 再进入,服务不工作了。调试发现 onStartCommand 每次都被调用,但上传的 Intent 被丢了。后来我把启动逻辑移到 onStartCommand,问题解决。
还有一次,我用了 START_STICKY,服务被系统杀死后自动重启,但 Intent 为 null,导致上传路径为空。换成 START_REDELIVER_INTENT 后,即使被杀,重启后也能拿到上次的路径。当然,如果你的 Intent 很大,频繁重启会浪费内存,需要权衡。
一句话总结:onCreate 初始化,onStartCommand 处理每次启动,onDestroy 清理善后。返回值和前台通知时机是两大魔鬼细节。