第 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+ 系统会检测并标记为“异常耗电”应用,几乎必被杀。请放弃此方案。
常见坑与最佳实践
startForeground5 秒未调用:Android 8+ 用startForegroundService启动后 5 秒内必须startForeground,否则抛ANR与RemoteServiceException。- 后台启动 Service 崩溃:Android 8+ 处于后台时
startService抛异常。改用ContextCompat.startForegroundService。 onStartCommand返回值乱写:常驻服务(如音乐)应返回START_STICKY,一次性任务应返回START_NOT_STICKY。onBind返回 null 但当作 Bound Service 用:客户端bindService会立刻断开。Bound Service 必须返回非空 IBinder。stopSelf与stopService误用:stopSelf(startId)只停对应任务,stopSelf()停所有任务。多任务并发时不要乱用。- 前台服务通知未设
setOngoing(true):用户可能滑动清除通知导致前台服务状态异常。 - 未声明
foregroundServiceType:Android 14+ 直接抛MissingForegroundServiceTypeException。 - 跨进程 Service 漏写
android:process:默认与 App 同进程,若想独立进程需android:process=":remote"。 - 绑定 Service 在 Activity 销毁前未 unbind:会泄漏 ServiceConnection,引发 ANR。
- 依赖 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 测试。