A
第 17 章JAVA40 分钟

Service 与后台任务

掌握 Started/Bound 两类 Service 的差异、startService 与 bindService、前台服务 startForeground、onStartCommand 返回值与 Android 8+ 后台限制与替代方案

学习目标

  • 区分 Started Service 与 Bound Service 的启动方式与生命周期
  • 掌握 startForeground 与前台服务通知的规范使用
  • 理解 onStartCommand 返回值的三种含义及适用场景
  • 了解 Android 8+ 后台执行限制对 Service 的影响与替代方案

学习目标

Service 是 Android 中“无 UI 的后台运行组件”,常用于播放音乐、上传文件、长连接维护等。本章目标:

  • 区分 Started Service 与 Bound Service 的启动方式与生命周期;
  • 用 startForeground 把后台 Service 升级为前台服务,避免被系统杀;
  • 理解 onStartCommand 返回值(START_STICKY / START_NOT_STICKY / START_REDELIVER_INTENT);
  • 了解 Android 8+ 后台执行限制与“保活”的合规替代方案。

Service 类型概览

类型 启动方式 退出条件 典型场景
Started startService 调用 stopSelf 或 stopService 上传、下载、播放
Bound bindService 所有 Client unbind 客户端 - 服务通信
混合 两者同时 都满足才销毁 IM 后台 + UI 绑定
<!-- AndroidManifest.xml -->
<service
    android:name=".UploadService"
    android:exported="false" />

<service
    android:name=".MusicService"
    android:exported="false"
    android:foregroundServiceType="mediaPlayback" />

Started Service

1. 启动与停止

public class UploadService extends Service {

    private static final String TAG = "UploadService";
    public static final String EXTRA_FILE_PATH = "extra_file_path";

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand: startId=" + startId);
        String filePath = intent != null ? intent.getStringExtra(EXTRA_FILE_PATH) : null;
        if (filePath != null) {
            doUploadAsync(filePath, startId);
        } else {
            stopSelf(startId);
        }
        return START_NOT_STICKY;
    }

    private void doUploadAsync(String filePath, int startId) {
        new Thread(() -> {
            boolean success = uploadFile(filePath);
            Log.d(TAG, "upload result: " + success);
            stopSelf(startId); // 完成后停止自身
        }).start();
    }

    private boolean uploadFile(String path) {
        try {
            Thread.sleep(3000);
            return true;
        } catch (InterruptedException e) {
            return false;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null; // Started Service 不提供绑定
    }

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

启动:

Intent intent = new Intent(this, UploadService.class);
intent.putExtra(UploadService.EXTRA_FILE_PATH, "/sdcard/file.jpg");
ContextCompat.startForegroundService(this, intent);
// 或 Android 8 之前用 startService(intent)

2. onStartCommand 返回值

返回值 含义 适用场景
START_STICKY 被杀后重建但 intent 为 null 后台常驻、不依赖具体 Intent
START_NOT_STICKY 被杀后不重建 一次性任务,丢失无所谓
START_REDELIVER_INTENT 被杀后重建并重发最后一个 Intent 必须执行完毕的任务

3. 多次启动的 startId

每次 startService 都会回调一次 onStartCommand 并分配一个新的 startId。stopSelf(startId) 表示“只停止这次”,stopSelf() 表示停止所有。生产环境应优先用 stopSelf(startId) 避免误停其他进行中的任务。

Bound Service

1. 通过 Binder 暴露接口

public class MusicService extends Service {

    private final IBinder mBinder = new LocalBinder();
    private MediaPlayer mPlayer;

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

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

    public void play(String url) {
        if (mPlayer == null) {
            mPlayer = new MediaPlayer();
            mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        }
        try {
            mPlayer.reset();
            mPlayer.setDataSource(url);
            mPlayer.prepare();
            mPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void pause() {
        if (mPlayer != null && mPlayer.isPlaying()) {
            mPlayer.pause();
        }
    }

    @Override
    public void onDestroy() {
        if (mPlayer != null) {
            mPlayer.release();
            mPlayer = null;
        }
        super.onDestroy();
    }
}

2. 客户端绑定

public class PlayerActivity extends AppCompatActivity {

    private MusicService mService;
    private boolean mBound = false;

    private final ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            MusicService.LocalBinder binder = (MusicService.LocalBinder) service;
            mService = binder.getService();
            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mBound = false;
        }
    };

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, MusicService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBound) {
            unbindService(mConnection);
            mBound = false;
        }
    }

    private void onPlayClick() {
        if (mBound && mService != null) {
            mService.play("https://example.com/song.mp3");
        }
    }
}

前台服务(Foreground Service)

1. 为什么需要前台服务

Android 6+ Doze 模式与 Android 8+ 后台限制使后台 Service 容易被杀。把 Service 升级为前台服务可常驻通知栏,享有更高优先级,不容易被回收。

2. 启动前台服务

public class DownloadService extends Service {

    private static final int NOTIFY_ID = 5001;
    private NotificationManagerCompat mNm;

    @Override
    public void onCreate() {
        super.onCreate();
        mNm = NotificationManagerCompat.from(this);
        ensureChannel();
    }

    private void ensureChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    "channel_fg", "下载服务", NotificationManager.IMPORTANCE_LOW);
            getSystemService(NotificationManager.class).createNotificationChannel(channel);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Notification noti = new NotificationCompat.Builder(this, "channel_fg")
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle("正在下载")
                .setContentText("准备中...")
                .setProgress(0, 0, true)
                .setOngoing(true)
                .build();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            startForeground(NOTIFY_ID, noti,
                    ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
        } else {
            startForeground(NOTIFY_ID, noti);
        }

        // 必须在 5 秒内调用 startForeground
        new Thread(() -> {
            for (int i = 0; i <= 100; i += 10) {
                updateProgress(i);
                try { Thread.sleep(500); } catch (InterruptedException ignored) { }
            }
            stopForeground(true);
            stopSelf();
        }).start();

        return START_NOT_STICKY;
    }

    private void updateProgress(int progress) {
        Notification noti = new NotificationCompat.Builder(this, "channel_fg")
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle("正在下载")
                .setContentText(progress + "%")
                .setProgress(100, progress, false)
                .setOnlyAlertOnce(true)
                .setOngoing(true)
                .build();
        mNm.notify(NOTIFY_ID, noti);
    }

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

3. Manifest 声明权限与类型

<!-- Android 9+ 需要前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14+ 需要按类型声明 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<service
    android:name=".DownloadService"
    android:exported="false"
    android:foregroundServiceType="dataSync" />

4. Android 14+ 类型枚举

类型 用途
mediaPlayback 音乐播放
dataSync 上传 / 下载
mediaProjection 屏幕录制
location 导航、运动跟踪
camera / microphone 直播、视频会议
health 健康监测
connectedDevice 智能家居

Android 8+ 后台执行限制

1. 限制内容

  • 后台 Service 启动:Android 8+ 上,处于后台的 App 调用 startService() 会抛 IllegalStateException,必须改用 startForegroundService() 并在 5 秒内调 startForeground()。
  • 隐式广播限制:大多数系统隐式广播(如 CONNECTIVITY_CHANGE)不再能通过 Manifest 静态注册接收。
  • Doze 与 Standby:长时间空闲后网络、闹钟、Job 都会被延迟到维护窗口。

2. 替代方案对照

场景 旧方案 推荐方案
后台上传/下载 Started Service + 线程 WorkManager / Foreground Service
定时同步 AlarmManager + Service WorkManager PeriodicWork
IM 长连接 Service + 保活 Foreground Service + FCM 推送唤醒
任务调度 JobScheduler WorkManager(兼容到 API 14)

保活策略与替代方案

1. 真正“保活”已不可能

Android 自 8.0 起对后台 Service 持续收紧,9.0 引入应用待机分组,12 进一步限制前台服务启动条件,“进程不死”几乎不可能。

2. 替代方案

  • 业务消息唤醒:用 FCM 高优先级 data 消息触发启动 Service;
  • 前台服务:仅在用户感知时使用(音乐、导航、运动、直播);
  • WorkManager:定时同步、清理等不敏感延迟的任务;
  • AlarmManager:精确闹钟(Android 12+ 需 SCHEDULE_EXACT_ALARM 权限);
  • JobScheduler:API 23+ 系统调度,WorkManager 已封装。

3. 1 像素 Activity 保活已死

早期国内厂商曾流行“屏幕关闭时启动 1 像素透明 Activity 防止休眠”,Android 9+ 系统会检测并标记为“异常耗电”应用,几乎必被杀。请放弃此方案。

常见坑与最佳实践

  1. startForeground 5 秒未调用:Android 8+ 用 startForegroundService 启动后 5 秒内必须 startForeground,否则抛 ANR 与 RemoteServiceException。
  2. 后台启动 Service 崩溃:Android 8+ 处于后台时 startService 抛异常。改用 ContextCompat.startForegroundService。
  3. onStartCommand 返回值乱写:常驻服务(如音乐)应返回 START_STICKY,一次性任务应返回 START_NOT_STICKY。
  4. onBind 返回 null 但当作 Bound Service 用:客户端 bindService 会立刻断开。Bound Service 必须返回非空 IBinder。
  5. stopSelf 与 stopService 误用:stopSelf(startId) 只停对应任务,stopSelf() 停所有任务。多任务并发时不要乱用。
  6. 前台服务通知未设 setOngoing(true):用户可能滑动清除通知导致前台服务状态异常。
  7. 未声明 foregroundServiceType:Android 14+ 直接抛 MissingForegroundServiceTypeException。
  8. 跨进程 Service 漏写 android:process:默认与 App 同进程,若想独立进程需 android:process=":remote"。
  9. 绑定 Service 在 Activity 销毁前未 unbind:会泄漏 ServiceConnection,引发 ANR。
  10. 依赖 Service 做 IM 保活:会被系统杀死导致消息丢失,必须配合 FCM/厂商推送。

章节小结

  • Started Service 用 startService 启动,Bound Service 用 bindService 绑定,混合 Service 同时支持两者;
  • onStartCommand 返回值决定被杀后是否重建及 Intent 是否重发;
  • Android 8+ 后台限制要求后台 Service 必须升级为前台服务(startForegroundService + startForeground + 通知);
  • Android 14+ 必须声明 foregroundServiceType 与对应权限;
  • “进程保活”在合规环境下已不可能,应改用 FCM/WorkManager/前台服务/AlarmManager 的组合。

下一章预告

第 18 章将讲解 WorkManager 任务调度:为什么用它替代 Service 与 JobScheduler,OneTimeWorkRequest / PeriodicWorkRequest、Constraints、链式任务(then / combine)、Worker 类实现、输入输出数据与 Worker 测试。