Service 的三种形态与后台任务调度
为什么 Service 要单独拎出来讲
Activity 有界面,用户离开就可能被系统回收;BroadcastReceiver 执行完onReceive就结束了。但有些任务,必须在用户离开界面后继续活着——音乐播放、文件下载、传感器监听。
这就是 Service 要解决的核心问题:让任务在没有 UI 的情况下持续运行。
但 Service 不是简单的"后台进程"。Android 给它设计了三种非常不同的形态,弄混了不仅做不到目标,还会浪费电量、被系统杀死,甚至在 Android 14 上直接抛异常。
启动服务:fire-and-forget 的任务执行
startService 的基本语义
最简单的一种形态:调用方触发、Service 自行运转、不需要回调结果。
Context.startService(intent)
│
▼ 跨 Binder 调用
ActivityManagerService (AMS)
│
▼
ServiceRecord 是否存在?
├── 否 → 创建进程 + 创建 Service 实例
└── 是 → 直接回调
│
▼
Service.onCreate() ← 只调用一次
Service.onStartCommand() ← 每次 startService 都触发
onStartCommand() 是整个流程的心脏,它的返回值决定了系统如何对待这个 Service:
class DownloadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 关键一:用 startId 标记本次请求,而不是直接 stopSelf()
// 这样可以避免提前停掉仍有任务的 Service
doWorkInBackground {
stopSelf(startId) // 只有本次任务完成后才停止
}
// 返回值决定系统杀死后的行为
return START_STICKY // ← 被杀后自动重启,但 intent 为 null
}
}
三个返回值的语义区别:
| 返回值 | 被系统杀死后 | intent 是否保留 | 适用场景 |
|---|---|---|---|
START_STICKY |
自动重启 | 不保留(为 null) | 音乐播放,状态在别处维护 |
START_NOT_STICKY |
不重启 | — | 周期性任务,下次触发自然会再 start |
START_REDELIVER_INTENT |
自动重启 | 保留原始 intent | 文件下载,必须知道下载哪个文件 |
为什么纯 startService 已经成为"过去式"
Android 8.0(Oreo)打破了一切:在后台状态下调用 startService() 会抛出 IllegalStateException。
Google 的逻辑是:看不见的 Service 消耗电量,用户无从察觉,手机莫名其妙没电——这个体验太差了。从此,你只有两个合法选择:
- 前台服务:必须显示通知,让用户知道
- WorkManager:系统统一调度,延迟执行但保证完成
前台服务:让 Service 对用户可见
为什么前台服务能"活下去"
Android 进程优先级从高到低:
前台进程(有可见 Activity 或前台 Service)
可见进程(有不在焦点但可见的 Activity)
服务进程(有运行中的 Service) ← 普通服务在这里
后台进程(所有 Activity 都在 onStop)
空进程(仅为缓存)
前台 Service 所在进程的优先级等同于前台进程,系统几乎不会主动杀它。代价是:必须显示一条持续通知。
Android 14 对前台服务的重大约束
Android 14 要求必须在 <service> 标签中声明 foregroundServiceType,否则抛出异常:
<service
android:name=".MusicService"
android:foregroundServiceType="mediaPlayback" />
支持的类型及含义:
| 类型 | 典型场景 | 是否需要权限 |
|---|---|---|
mediaPlayback |
音乐/视频播放 | 否 |
location |
GPS 导航 | ACCESS_FINE_LOCATION |
camera |
后台拍摄 | CAMERA |
microphone |
后台录音 | RECORD_AUDIO |
dataSync |
数据同步 | (Android 15 已废弃!) |
health |
健康传感器 | (Android 14 新增) |
正确启动前台服务的完整代码:
class MusicService : Service() {
override fun onCreate() {
super.onCreate()
// 必须在 onStartCommand 执行前完成,否则 ANR 风险
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
// Android 10+ 需要第三个参数声明 serviceType
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
return START_STICKY
}
private fun buildNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在播放")
.setSmallIcon(R.drawable.ic_music)
.build()
}
}
重要细节:startForeground() 必须在服务创建后的几秒内调用,否则系统的 ANR 看门狗会杀掉服务。在慢速启动路径(需要初始化大量资源)上,要注意这个时序问题。
绑定服务:双向通信的客户端-服务器模式
绑定服务解决的核心问题
启动服务是单向的:调用方发出指令,Service 去干活,但没有办法知道结果、中途修改行为、或实时查询状态。
绑定服务解决了这个问题。它建立一个持久的双向通道,调用方可以像调用本地对象的方法一样与 Service 交互。
比喻:启动服务像往工厂投递订单,绑定服务像在工厂现场驻场监督。
IBinder 是双向通道的核心
class MusicService : Service() {
// 内部类持有 Service 的引用,让调用方能访问 Service 的方法
inner class MusicBinder : Binder() {
fun getService(): MusicService = this@MusicService
}
private val binder = MusicBinder()
// 这是绑定服务的核心:onBind 返回 IBinder
// 调用方通过它与 Service 通信
override fun onBind(intent: Intent): IBinder = binder
// Service 暴露给调用方的公开方法
fun play(songId: String) { /* ... */ }
fun pause() { /* ... */ }
fun getProgress(): Int = currentProgress
}
调用方绑定并使用:
class PlayerActivity : AppCompatActivity() {
private var musicService: MusicService? = null
private var isBound = false
private val connection = object : ServiceConnection {
// 连接建立时回调,IBinder 就是 Service 返回的那个对象
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as MusicService.MusicBinder
musicService = binder.getService()
isBound = true
}
// 连接意外断开时(如 Service 崩溃),不是主动解绑
override fun onServiceDisconnected(name: ComponentName) {
isBound = false
}
}
override fun onStart() {
super.onStart()
Intent(this, MusicService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
if (isBound) {
unbindService(connection)
isBound = false
}
}
}
绑定服务的生命周期规则
关键规则:当所有客户端都解绑后,且服务没有通过 startService() 启动,系统会自动销毁绑定服务。
客户端绑定 → onBind() → onServiceConnected()
客户端解绑 → onUnbind() → (若无其他绑定且未 start)onDestroy()
特例:
startService() + bindService() 同时调用
→ 需要同时 stopService() + unbindService() 才会触发 onDestroy()
跨进程绑定服务:AIDL 的用武之地
同进程内用 Binder 子类直接通信就够了。跨进程通信需要 AIDL(Android Interface Definition Language):
// IMusicControl.aidl
interface IMusicControl {
void play(String songId);
void pause();
int getProgress();
}
系统会根据 AIDL 文件自动生成 Stub(服务端基类)和 Proxy(客户端代理),它们底层走的是 Binder 内核驱动——序列化参数 → 写入内核缓冲区 → 对端读取 → 反序列化。这套机制让跨进程调用看起来像本地调用。
什么时候用 AIDL?
- 需要对外(其他 App)提供服务接口
- 同进程内优先用普通 Binder
startService 与 bindService 的内部流程(源码视角)
两条路都要经过同一个守门员:ActivityManagerService。
// startService 路径(简化)
App 进程:
ContextImpl.startService(intent)
└─ 通过 Binder 调用 ──────────────────────────────►
system_server 进程:
AMS.startService()
└─ ActiveServices.startServiceLocked()
│
① 查 ServiceRecord(没有就创建)
② 检查目标进程是否存在
③ 发送 MSG_START_SERVICE 消息
│
App 进程(通过 ApplicationThread):
H.handleCreateService()
→ Service.onCreate()
→ Service.onStartCommand()
ApplicationThread 是 AMS 回调 App 进程的通道(也是 Binder),H 是 App 主线程上的 Handler。所有生命周期回调最终都在主线程上执行,这就是为什么不能在 onStartCommand 里做耗时操作的原因。
bindService 的背后:Binder 双向连接的建立
绑定服务的连接建立比启动服务更复杂,因为它需要把 IBinder 对象从 Service 进程传回客户端进程:
客户端进程: system_server (AMS): Service 进程:
bindService(intent, conn, flags)
│
└─ AMS.bindService() ──►
查 ServiceRecord
检查 Service 是否创建
Service 未运行 → 启动 Service
│
└─────────────────────► Service.onBind()
│ 返回 IBinder
◄─────────────────────────────┘
AMS 保存 IBinder 引用
◄── publishService() ────
│
ServiceConnection.onServiceConnected(IBinder)
这里有一个重要的优化:同一个 ServiceConnection 绑定同一个 Service,onBind() 只调用一次。后续再有其他客户端绑定,AMS 直接把已有的 IBinder 传过去,不再调用 onBind()。
后台任务的演进:从 Service 到 WorkManager
历史上的后台任务方案对比
AsyncTask (已废弃) → 只能与 Activity 生命周期绑定,旋转屏幕就崩
IntentService → 工作线程单任务队列,不支持约束、无持久化
AlarmManager → 精确定时但耗电,Doze 模式下被限制
JobScheduler → API 21+,支持约束,但需要手动处理兼容性
WorkManager → Jetpack 组件,API 14+ 兼容,持久化、可链式组合
WorkManager 的核心设计
WorkManager 解决的问题:可延迟的、可靠的后台任务调度。"可靠"的具体含义是:即使 App 被杀死或设备重启,任务仍然会在满足条件后执行完成。
它如何实现可靠性?把任务持久化到 Room 数据库。
WorkManager 内部架构:
WorkRequest → WorkManager → WorkDatabase (Room)
(任务描述) .enqueue() (任务持久化)
│
根据 API 级别选择调度器:
│
┌───────┴────────────────┐
▼ ▼
API 23+: API 14-22:
JobScheduler AlarmManager
+ BroadcastReceiver
WorkManager 不是"又一个后台任务框架",它是对现有系统 API 的智能封装,屏蔽了版本差异。
一次性任务与周期性任务
// 一次性任务
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络
.setRequiresStorageNotLow(true) // 存储空间充足
.build()
)
.setInputData(workDataOf("fileId" to "123"))
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.addTag("upload")
.build()
WorkManager.getInstance(context).enqueue(uploadRequest)
// 周期性任务(最小间隔 15 分钟)
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"daily_sync", // 唯一名称,防止重复
ExistingPeriodicWorkPolicy.KEEP, // 已有同名任务则保留
syncRequest
)
Worker 的正确写法
class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val fileId = inputData.getString("fileId") ?: return Result.failure()
return try {
// 上传逻辑(可以是耗时操作,在工作线程执行)
val success = uploadFile(fileId)
if (success) {
// 向调用方传递结果数据
val output = workDataOf("uploadedUrl" to "https://...")
Result.success(output)
} else {
Result.retry() // 触发退避重试
}
} catch (e: Exception) {
Result.failure() // 永久失败,不重试
}
}
}
CoroutineWorker 是 WorkManager 提供的协程支持,doWork() 在后台线程上执行,超时后(默认 10 分钟)系统会停止 Worker 并标记为 FAILURE。
任务链
WorkManager 最强大的特性之一——任务链:
// 先压缩,再上传,结果发送通知
WorkManager.getInstance(context)
.beginWith(compressRequest) // 第一步
.then(uploadRequest) // 第二步(用第一步的输出作为输入)
.then(notifyRequest) // 第三步
.enqueue()
// 并行前置任务:多个压缩任务同时跑,全部完成后才合并上传
val parallelWork = listOf(compressSmall, compressLarge)
WorkManager.getInstance(context)
.beginWith(parallelWork) // 并行执行
.then(mergeAndUpload) // 全部完成后执行
.enqueue()
观察任务状态
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadRequest.id)
.observe(this) { workInfo ->
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> showSuccess()
WorkInfo.State.FAILED -> showError()
WorkInfo.State.RUNNING -> showProgress()
else -> {}
}
}
Service vs WorkManager 的选择标准
很多开发者犯的错误是:任何后台任务都用 Service,或者任何后台任务都用 WorkManager。
正确的选择矩阵:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 音乐播放、导航、持续录音 | 前台 Service | 需要实时、持续运行,必须通知用户 |
| 数据上传、日志同步 | WorkManager | 延迟可接受,需要在 App 退出/重启后继续 |
| 即时数据处理(点击按钮触发) | 协程 + ViewModel | 不需要跨生命周期持久化 |
| 精确定时任务(整点提醒) | AlarmManager | WorkManager 不保证精确时间 |
| 为其他 App 提供服务接口 | 绑定 Service + AIDL | IPC 场景 |
| 短暂的一次性后台处理 | WorkManager | 即使是短暂的也建议用,保证可靠性 |
一句话总结:需要用户感知 + 实时 + 持续 → 前台 Service;需要可靠完成 + 不在乎时机 → WorkManager;需要跨进程接口 → 绑定 Service。
核心底层逻辑解析
"Service 运行在哪个线程?"
onCreate()、onStartCommand()、onBind() 都在主线程(UI 线程)上执行。如果要做耗时操作,必须自己开线程,或者用 CoroutineWorker(WorkManager 里)。
这也是为什么经典的 IntentService 存在的原因——它内部有一个 HandlerThread,自动在后台线程里处理任务。但 IntentService 已在 API 30 被废弃,应使用 WorkManager 替代。
"两个 Activity 绑定同一个 Service,onBind 会调用几次?"
只调用一次。AMS 会缓存第一次调用 onBind() 返回的 IBinder,后续绑定直接复用。但如果两个 Activity 使用不同的 intent(比如带不同的 action),系统可能调用 onBind() 两次——取决于 onRebind() 和 onUnbind() 的返回值。
"WorkManager 的任务一定会执行吗?"
"一定"的前提是:满足约束条件。WorkManager 保证任务在约束满足后最终会执行,但不保证时间。另外,如果用户在设置里对 App 执行"强制停止",当前队列中的任务会被清除(这是 Android 系统级行为,任何库都无法绕过)。