前台服务生命周期详解:onCreate、onStartCommand、onDestroy 回调顺序与注意事项

前台服务的生命周期,说白了就是三个核心回调:onCreate、onStartCommand、onDestroy。很多刚接触 Service 的开发者容易混淆它们的调用时机,或者认为和 Activity 类似。嗯,这里我要提醒一句:**Service 的生命周期要简单得多,但坑往往藏在细节里**。我个人习惯在写每个前台服务前,先把这三个回调的时序图画在纸上,再动手编码。

核心认知:前台服务的生命周期不依赖于 Activity 或 Fragment,它由系统进程管理。一旦启动,即使 App 切到后台,服务仍在运行(除非被系统杀死)。

1. 回调执行顺序

你想想看,一个前台服务从出生到销毁,走的是这样的流程:

  1. onCreate() – 仅一次:第一次通过 startService() 或 startForegroundService() 启动服务时调用。如果服务已经创建过,即使多次 start,onCreate 也只走一次。
  2. onStartCommand() – 每次 start 都调用:每次有人调用 startService() 或 startForegroundService() 都会触发。注意,系统可能因资源紧张杀掉服务后重启,重新调用 onStartCommand 并传入上一次的 Intent。
  3. onDestroy() – 仅一次:当 stopService() 或 stopSelf() 调用后,服务结束生命周期。如果服务被系统强制杀死,onDestroy 可能不执行(嗯,这里是个大坑)。

我曾在项目中遇到过一个问题:重复调用 startForegroundService() 导致 onStartCommand 执行多次,而 onCreate 只执行一次。我在 onCreate 里初始化了一个定时器,结果定时器只被创建一次,后续 start 拿到的 Intent 却不被处理——因为我把启动逻辑写在 onCreate 里了。**经验教训:启动逻辑要放在 onStartCommand 中,onCreate 只做一次性资源初始化。**

警告:千万不要在 onCreate 里做耗时操作(超过几毫秒),因为前台服务的主线程就是 UI 线程(除非你单独开线程)。我见过有人直接在 onCreate 里做网络请求,结果 ANR。前台服务虽然优先级高,但依然受主线程超时限制。

前台服务生命周期回调流程 onCreate() onStartCommand(intent, flags, startId) 可被多次调用 每次 startService 都触发 多次 start onDestroy() 触发条件: stopService() 或 stopSelf() 系统被杀则可能不回调 ⚠ 注意事项 onCreate 只执行一次,onStartCommand 可多次执行 启动逻辑务必放在 onStartCommand,而非 onCreate

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 清理善后。返回值和前台通知时机是两大魔鬼细节。