18、Service基础:启动服务、绑定服务、IntentService、前台服务与通知栏

Service,说白了就是Android里跑在后台的“隐形打工人”。

它没有界面,但能干很多脏活累活——下载文件、播放音乐、处理网络请求。我刚开始学Android时,总觉得Service和线程差不多,后来踩了坑才明白,这两者压根不是一回事。

18.1 什么是Service?

Service是Android四大组件之一。它设计用来执行长时间运行的后台操作。注意,它运行在主线程,这一点很多人会搞混。

你想想看,如果Service里直接写个网络请求,那不就ANR了吗?嗯,这里要注意:Service本身不创建线程,你得自己在Service里开子线程干活。

核心要点:Service ≠ 后台线程。Service是组件,线程是执行单元。两者配合使用才是正解。

18.2 启动服务 vs 绑定服务

Service有两种使用方式,我习惯把它们比作“临时工”和“长期顾问”。

18.2.1 启动服务(Started Service)

通过 startService() 启动。一旦启动,Service会一直运行,直到调用 stopSelf() 或外部调用 stopService()。调用者和Service之间没有直接通信。

我在项目中遇到过这样一个场景:用户上传图片,我启动一个Service去处理。用户切到别的App,Service还在跑。这就是启动服务的典型用法。

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

// Service内部
public class MyUploadService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 这里要开线程干活
        new Thread(() -> {
            // 执行上传逻辑
            stopSelf(); // 干完活自己停掉
        }).start();
        return START_STICKY; // 被杀死后自动重启
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null; // 启动服务不需要绑定
    }
}

我的习惯:返回 START_STICKY 还是 START_NOT_STICKY?如果任务必须完成,用STICKY;如果任务可以中断,用NOT_STICKY。我曾经因为用了STICKY导致Service反复重启,浪费了不少流量。

18.2.2 绑定服务(Bound Service)

通过 bindService() 绑定。调用者和Service之间可以双向通信。当所有调用者都解绑后,Service自动销毁。

说白了,绑定服务就是“你和我说话,我回你话”。适合做音乐播放器这种需要交互的场景。

// 绑定服务
Intent intent = new Intent(this, MyMusicService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);

private ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        // 拿到Binder,可以调用Service的方法
        MyBinder binder = (MyBinder) service;
        binder.play();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        // 服务意外断开
    }
};

// Service内部
public class MyMusicService extends Service {
    private final IBinder binder = new MyBinder();

    public class MyBinder extends Binder {
        public void play() {
            // 播放音乐
        }
    }

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

注意:绑定服务是异步的。你调用 bindService() 后,不能立刻拿到Binder。我曾经犯过这个错——在 onCreate 里直接调用绑定服务的方法,结果空指针了。

18.3 IntentService:省心版Service

还记得我刚才说的吗?Service里要自己开线程。但IntentService帮你做了这件事。

IntentService内部维护了一个工作线程和一个队列。每次启动,它会创建一个新的Intent,放到队列里,然后依次处理。处理完一个,自动停掉。

我特别喜欢IntentService的一点:你不用操心线程管理,也不用担心忘记 stopSelf()

public class MyDownloadService extends IntentService {

    public MyDownloadService() {
        super("MyDownloadService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // 这里已经在子线程了,放心干活
        String url = intent.getStringExtra("url");
        downloadFile(url);
    }
}

避坑指南:IntentService在Android 8.0(API 26)之后被标记为废弃。官方推荐用 JobIntentService 或直接使用 WorkManager。但我个人觉得,理解IntentService的设计思想,对理解后台任务模型很有帮助。

18.4 前台服务与通知栏

从Android 8.0开始,后台Service的限制越来越严。你想想看,如果每个App都在后台乱跑,用户的手机电量撑得住吗?

所以,Google推出了前台服务。前台服务会显示一个通知栏,告诉用户“我正在干活”。这样用户就知道你的App在后台做什么。

我记得有一次,用户反馈说App播放音乐时突然停了。查了半天,原来是Service被系统杀掉了。后来改成前台服务,问题就解决了。

// 创建通知渠道(Android 8.0+ 必须)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    NotificationChannel channel = new NotificationChannel(
        "music_channel",
        "音乐播放",
        NotificationManager.IMPORTANCE_LOW
    );
    NotificationManager manager = getSystemService(NotificationManager.class);
    manager.createNotificationChannel(channel);
}

// 构建通知
Notification notification = new NotificationCompat.Builder(this, "music_channel")
    .setContentTitle("正在播放")
    .setContentText("周杰伦 - 七里香")
    .setSmallIcon(R.drawable.ic_music)
    .setOngoing(true) // 不可滑动删除
    .build();

// 启动前台服务
startForeground(1, notification);

我的经验:前台服务的通知一定要设置 setOngoing(true)。否则用户不小心滑掉通知,Service可能被系统杀掉。另外,通知的 importance 建议用 LOWMIN,别打扰用户。

18.5 知识体系总览

下面这张图,是我梳理的Service核心知识结构。你看一眼,心里就有谱了。

Service 启动服务 绑定服务 IntentService 前台服务 startService stopSelf bindService ServiceConnection onHandleIntent 自动停止 startForeground 通知栏 Android Service 核心知识体系

18.6 生命周期对比

不同Service的生命周期差异很大。我整理了一张表,方便你对照。

类型 启动方式 生命周期 通信方式 典型场景
启动服务 startService() 独立于调用者,需手动停止 无直接通信 文件下载、上传
绑定服务 bindService() 随调用者生命周期 Binder双向通信 音乐播放器、计时器
IntentService startService() 任务完成后自动停止 无直接通信 简单后台任务队列
前台服务 startForeground() 高优先级,不易被杀死 通知栏展示 音乐播放、位置追踪

18.7 避坑总结

最后,分享几个我踩过的坑,你遇到了能少走弯路。

  • 不要在Service里直接做耗时操作。Service跑在主线程,你写个网络请求,几秒后ANR就来了。
  • Android 8.0以上,后台Service限制严格。能上前台服务就上前台服务,别硬扛。
  • IntentService已废弃,但思想值得学。新项目建议用WorkManager或JobIntentService。
  • 绑定服务记得解绑。在Activity的 onDestroy 里调用 unbindService(),否则会泄漏。
  • 前台服务的通知一定要有渠道。Android 8.0以上,不创建渠道,通知不显示。

嗯,Service这块内容就这些。说白了,它就是Android后台能力的基石。你把它搞明白了,后面学WorkManager、JobScheduler都会轻松很多。


公众号:蓝海资料掘金营,微信deep3321