Android Service 基础:生命周期、启动方式、绑定方式

讲 Foreground Service 之前,咱们得先把普通 Service 的基础摸透。说白了,Service 就是后台干活的一个组件,没有界面。你想想看,一个没有 UI 的东西,它怎么活?怎么被人调用?生命周期怎么走?这些搞不清楚,后面的长时间任务根本没法落地。

我刚开始接触 Service 时,踩过一个坑:以为 Service 和 Activity 一样,onCreate → onStart → onResume 那一套。结果发现完全不是那么回事。它的生命周期更简单,但也更容易在绑定和启动混合使用时绕晕。今天咱们把这几个核心点理清。

一、Service 的生命周期

Service 的生命周期只有两个关键路径:

  • 启动式(Started):通过 startService() 启动,独立运行,直到调用 stopSelf() 或外部 stopService()。
  • 绑定式(Bound):通过 bindService() 启动,生命周期与绑定它的客户端(如 Activity)绑定,所有客户端解除绑定后自动销毁。

但实际情况中,Service 可能同时处于“已启动”和“已绑定”状态。这时候生命周期怎么走?看这张图就清楚了。

Android Service 生命周期与启动/绑定方式 Service 实例 startService() onStartCommand() (可多次调用) bindService() onBind() 返回 IBinder Service 运行中 stopSelf() / stopService() onDestroy() 服务终止 所有客户端解绑 onUnbind() + onDestroy() 注意:Service 可同时处于“已启动”和“已绑定”状态,销毁时机取两者中最后完成的那个
个人习惯:我通常在测试 Service 生命周期时,每个回调方法里都打 Log 带当前线程 ID,这样一眼就能看出是不是在 UI 线程。Service 默认和主线程跑在一起,容易阻塞。

二、启动方式详解

启动 Service 最直接的方式就是 startService(Intent)。调一次,onStartCommand 就会执行一次。注意是“每次调用都执行”,但 onCreate 只在第一次创建时执行。

// 启动服务
Intent intent = new Intent(this, MyService.class);
startService(intent);

// 服务内部
public class MyService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("TAG", "Service 创建");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("TAG", "收到 start 命令");
        // 在这里做耗时工作,记得另起线程
        return START_STICKY; // 被系统杀掉后自动重启
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("TAG", "Service 销毁");
    }
}

onStartCommand 的返回值决定了 Service 被系统杀死后的重启策略。我项目里常用的有三种:

返回值常量 含义 我遇到的情况
START_STICKY 被杀后自动重启,但 intent 为 null 适合纯后台轮询任务,比如位置上报
START_NOT_STICKY 被杀后不重启 适合一次性任务,比如上传日志
START_REDELIVER_INTENT 被杀后重新传递最后一次 intent 适合关键任务,比如消息推送确认
避坑指南:我曾经因为用了 START_STICKY 导致服务被杀后不断空启动,一检测 intent == null 才发现的。如果你要依赖 intent 里的数据,请用 START_REDELIVER_INTENT。

三、绑定方式详解

绑定方式用于在组件和服务之间建立一条“通道”。实现方式是在 Service 里创建一个 Binder 对象,通过 onBind() 返回给客户端。客户端拿到 Binder 后可以直接调用 Service 里的方法。

我个人习惯把绑定分为两种场景:本地绑定和远程绑定(通过 AIDL)。本地绑定最简单,Service 和客户端在同一个进程时用。

// Service 端
public class LocalService extends Service {
    private final IBinder binder = new LocalBinder();

    public class LocalBinder extends Binder {
        LocalService getService() {
            return LocalService.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    public void doHeavyWork() {
        // 实际工作方法
    }
}

// Activity 端
private LocalService service;
private ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
        service = ((LocalService.LocalBinder) binder).getService();
        service.doHeavyWork();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        service = null;
    }
};

@Override
protected void onStart() {
    super.onStart();
    bindService(new Intent(this, LocalService.class), connection, Context.BIND_AUTO_CREATE);
}

@Override
protected void onStop() {
    super.onStop();
    unbindService(connection);
}

重点:绑定服务时一定要在合适的生命周期里解绑,比如 Activity 的 onStop 或 onDestroy。否则会造成泄漏,Service 永远无法销毁。我记得有一次线上崩溃就是因为在 Dialog 里绑了服务没解绑,导致 Activity 泄漏了……

四、混合使用:即启动又绑定

很多实际场景下,同一个 Service 既需要被 startService 长时间运行,又需要支持外部组件绑定获取数据。这种情况下,生命周期管理要特别小心。

  • 停止时机:只有同时满足“没有客户端绑定”且“没有调用 stopSelf”时,Service 才会真正销毁。onDestroy 只会被调用一次。
  • 推荐做法:启动后立刻在内部持有一个唤醒锁(WakeLock),防止 CPU 休眠。同时对外提供 Binder 查询当前状态。
  • 我的教训:以前写过一个下载服务,启动+绑定混合,结果因为忘记在 onDestroy 里释放 WakeLock,导致手机一晚上掉电 30%。从那以后我都在 onDestroy 里统一清理所有资源。

五、小结(用表格收个尾)

特性 启动方式 绑定方式
生命周期 由 stopSelf/stopService 控制 由客户端绑定数量控制
通信方式 通过 Intent 传递数据 通过 IBinder 直接调用方法
多次调用 onStartCommand 每次触发 onBind 只调用一次,但可连接多个客户端
典型场景 后台音乐播放、位置跟踪 与 UI 交互的下载任务、即时数据查询

说到底,Service 基础不复杂,复杂的是怎么跟 Activity 的屏幕旋转、后台杀死、内存不足这些现实问题结合起来处理。下一章我们开始专门搞 Foreground Service,这个才是真刀真枪的长时间任务测试。不过在此之前,你最好亲手写一个 Demo,把启动、绑定、混合三种情况都跑一遍,观察 Logcat 里的回调顺序——我当年就是这么彻底搞懂的。

一个小技巧:写测试代码时,可以用 adb shell dumpsys activity services 查看当前运行的 Service 列表和绑定状态,比 Log 更直观。