Appearance
Service:start 与 bind 双轨制、前台服务新规与退休名单
本页导读:本页讲 Service 的两套完全不同的语义(started 与 bound)、它们在 system_server 里的记账路径、Android 8~16 一层层收紧的前台服务(FGS)规则,以及 IntentService 为什么被淘汰、什么场景该换成 WorkManager/协程。前置:进程模型、Activity。
一、先分清两种 Service:started 与 bound
| 维度 | started(startService) | bound(bindService) |
|---|---|---|
| 语义 | 「替我干活」:发指令,不关心谁在线 | 「租你一个能力」:调用者活着服务就得活着 |
| 生命周期 | 自己管(stopSelf/stopService) | 随最后一个连接解绑而销毁 |
| 返回通道 | 单向,结果靠广播/回调 | IBinder 接口,直接方法调用 |
| 进程优先级 | SERVICE 档(负到正值细分,以 AOSP 为准) | 不低于绑定方(adj 传播) |
| 典型误用 | 用它做「后台常驻」 | 用它保活(下一节拆穿) |
两轨可以叠加:先 start 再 bind,则解绑不会杀死服务;stopSelf(startId) 带 id 判断,防止在「旧的 start 已被回收」时误停正在服务新请求的实例。
started Service + 协程的当代最小正确形态:
kotlin
class SyncService : Service() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
scope.launch {
runCatching { doSync(intent) } // 工作必须在 IO 调度器
stopSelf(startId) // 带 id,防止误停新任务
}
return START_NOT_STICKY // 可重放的交给 WorkManager,这里不硬扛
}
override fun onBind(i: Intent?) = null // 纯 started:bind 返回 null
override fun onDestroy() { scope.cancel() }
}铁律第一条:Service 的回调(onStartCommand/onBind/所有 Binder 调用)都跑在主线程。 Service 不是「后台线程容器」,它只是「后台存在的组件」——耗时逻辑必须自己挂到线程池/协程,否则就是在制造 ANR。
二、startService 在系统侧的记账路径
text
Context.startService / startForegroundService
└ Binder ──▶ ActiveServices.startServiceLocked(com.android.server.am)
├ 权限/后台限制校验(8.0 起后台 start 普通服务直接抛 IllegalStateException)
├ 进程不在 → AMS 拉进程(见进程模型),StartQueueRecord 排队
└ bringUpServiceLocked → scheduleService ── Binder(oneway) ──▶ 应用侧
ActivityThread.handleServiceArgs
└ Service.onStartCommand(...) 在主线程执行,返回 START_STICKY 系列超时会怎样?ActiveServices 给 Service 挂了两个计时器:
java
// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java(常量节选)
static final long SERVICE_TIMEOUT = 20 * 1000; // 前台服务绑定/启动 20s
static final long SERVICE_BACKGROUND_TIMEOUT = 200 * 1000; // 后台 200s
// 超时 → ANR:"Context.startForegroundService() did not then call startForeground()..."
// 或 "executing service ..."(具体文案随版本微调,以 AOSP 为准)bash
adb shell dumpsys activity services | head -40 # 看 app 的 started/bound 服务全账
# 关键行示例:
# ServiceState{...}: name=SyncService isForeground=true ... restartCount=3
# ConnectionRecord{...} flags=BIND_AUTO_CREATE ...onStartCommand 的返回值决定进程死后的重放策略:START_STICKY(重建但 intent 为 null)、START_REDELIVER_INTENT(重放最后 intent)、START_NOT_STICKY(不重建);重建有退避延时(ServiceState 的 restart delay,以 AOSP 为准)。
三、bindService 的连接簿记
bind 的本质是在 ActiveServices 里登记 ConnectionRecord/BindServiceRecord 两张账,并把目标进程 adj 拉进绑定方的优先级圈:
bindService→bindServiceLocked:权限与跨用户校验,建账,必要时拉起进程;scheduleBindService→ 应用侧onBind返回IBinder;ActiveServices.publishService→serviceConnectionLocked→scheduleConnection→ 调用者onServiceConnected;- 解绑
unbindService记账减一,归零且非 started 则销毁;绑定方进程死亡走 binderDied 回收,不会把服务方拖崩。
flags 决定「连接怎么影响生死」:
BIND_AUTO_CREATE:进程/服务未起则拉起(默认组合里几乎必带);BIND_IMPORTANT:把自己进程也拉进 important 档,慎用;BIND_WAIVE_PRIORITY/BIND_ADJUST_WITH_PRIORITY:控制 adj 传播方向;- Android 14+ 起跨包绑定还需目标
android:exported="true"且受BIND_ALLOW_...类新限制约束(以 AOSP 为准)。
跨进程拿到的 IBinder 只是代理,接口靠 AIDL 编译生成;服务侧要防「调用方死了代理还在」:
kotlin
class PlayerService : Service() {
private val binder = object : IPlayer.Stub() {
override fun play(uri: String) { player.play(uri) }
}
override fun onBind(intent: Intent?): IBinder = binder
// 调用方 binder 死亡由 Binder 框架回调 binderDied,
// 服务侧据此清理该客户端注册的 callback,避免泄漏
}四、前台服务:Android 8 → 16 的收紧年表
FGS 是「用户必须感知的持续工作」唯一合法通道,规则逐年加码:
text
Android 8 (API 26) 后台执行限制:后台 startService 受限;引入 startForegroundService,
起后须限时调 startForeground,否则崩(Android O 时代 5s,后续收紧为 10s 口径,以 AOSP ActiveServices 为准)
Android 10 (API 29) 引入 FGS 类型(location/camera/microphone…),使用对应硬件需 while-in-use 权限
Android 11 (API 30) startForegroundService 未限时 startForeground → 直接 RemoteServiceException
Android 12 (API 31) 禁止后台启动 FGS:抛 ForegroundServiceStartNotAllowedException(豁免清单见官方文档)
Android 13 (API 33) FGS 通知强制关联 POST_NOTIFICATIONS;无通知权限则不可见但仍计费
Android 14 (API 34) manifest 必须声明 android:foregroundServiceType,缺失即崩溃;
dataSync 类要求「用户可见触发」,纯后台同步被赶去 WorkManager
Android 15 (API 35) 媒体类 FGS 与播放状态核验加严(无声播放占位会被系统叫停)
Android 16 (API 36) while-in-use 型 FGS 进一步与可见性/权限绑定,进程退后台后受限执行(详见行为变更文档,细节以 AOSP 为准)正确姿势的完整清单:
- manifest:
android:foregroundServiceType="mediaPlayback"等 + 对应FOREGROUND_SERVICE_XXX权限; - 运行时:
startForegroundService→ 立即startForeground(id, notification),notification 必须合法(渠道、小图标); - 别用假通知骗系统:类型与行为不符(如用 dataSync 干定位)从 Android 14 起会被审计下架(Play 政策)+ 系统侧限制。
Android 12「后台禁起 FGS」的豁免清单(节选,完整清单以官方文档为准):应用可见(有任务栈可见 Activity)、收到自身广播/回调的短暂窗口、绑定到前台应用、用户交互产生的 PendingIntent、device owner、来电/短信类系统角色等——注意每一条都伴随短暂执行窗口(约 10s 量级,以 AOSP BroadcastQueue FGS 提权逻辑为准),不是永久门票。
五、bindService 保活为什么是幻觉
「绑定一个服务让对方进程不死」听起来像后门,实际上不存在:
- adj 传播只保证「对方不低于你」——你自己就是 cached 档时,把对方抬到 cached 没有任何保护力;
- 想把自己抬上去(BIND_IMPORTANT 等)需要
android.permission.BIND_...类特权或系统签名,三方拿不到; - 从最近任务划掉调用方进程组,整组连接一起回收;LMKD 按进程组算账,互相绑定反而一起死;
- 系统应用互相绑定(如输入法、无障碍)合法,是因为它们跑在特权域——域转换由 SELinux 控制,三方模仿不了。
结论:bind 的正当用途是能力共享与 RPC(播放控制、地图引擎、SDK 内部通道),不是保命。
一个反向提醒:被绑定的服务 SDK 也有义务「随叫随散」——onUnbind 后没有 started 就该立刻死掉,赖着不走只会让宿主 App 的省电评分和 ANR 榜一起变难看。
六、IntentService 退休了,然后呢
android.app.IntentService 在 API 30 被弃用(它用「自起 HandlerThread + wake lock」的旧模型解决串行后台任务),替代品按场景分:
| 场景 | 2026 年正确组件 |
|---|---|
| 一次性、可延迟的后台工作 | WorkManager(约束/重试/省电全托管) |
| 进程内串行任务 | 协程单线程调度器 / HandlerThread 自己写 |
| 需要「任务在但 UI 关了继续」 | 前台服务(带正确类型)或 WorkManager 的 expedited+FGS 桥接 |
| 崩溃也要重启长任务 | JobScheduler/JobInfo 持久化 + WorkManager |
| 曾经过渡用的 JobIntentService | 同样弃用,迁移到 WorkManager |
kotlin
// WorkManager 最小等价物:取代 IntentService 的「起服务跑一把就退出」
val req = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(ctx).enqueue(req) // 充电/联网约束、重试、省电全由系统调度IntentService 的源码思路仍值得一看:onStartCommand 入队 IntentWorkName、HandlerThread 串行消费、空闲 stopSelf——面试与考古两用(旧版源码在 frameworks/base/core/java/android/app/IntentService.java)。
另注意:WorkManager 不会为你突破 Doze——真正必须准点醒来的场景(闹钟、日程提醒)走 exact alarm + 用户授权,见广播页的限频一节。
七、Service 的 ANR、死亡与观测
- 超时表:前台 20s / 后台 200s 未完成启动调度即 ANR;FGS 未按期
startForeground直接崩溃;onBind阻塞同样计入。 - 死亡语义:started 服务进程被杀 → 按 restart 策略重建(intent 可能为 null);bound 服务死亡 → 调用方 binderDied/onServiceDisconnected,不会崩调用方。
- 看绑定方:
dumpsys activity service connections排查「SDK 偷偷 bind 服务」一抓一个准; - 观测手段:
adb shell dumpsys activity services:当前 started/bound 全表、restart 次数、ANR 候选;adb shell dumpsys activity oom_adjs思路(进程模型页)看服务进程 adj 来源;- perfetto 的
am数据源:service_started/service_boundslice,定位「服务起不来」卡在哪一段。
- 测试口径:
adb shell cmd deviceidle step推进 Doze 状态;userdebug 上可用am hang模拟回调卡死验证 ANR 文案;别在生产代码里留 sleep 兜底。
版本注记:基于 Android 16 / API 36;Android 8 前后台随意 startService、9 前无类型约束、12 前后台可起 FGS、14 前不声明类型也能跑——老教程里的 Service 玩法在 2026 年大部分已违法或无效。升级 targetSdk 时先跑 FGS 自查三问:类型声明了吗?后台能起吗?通知合规吗?
本页小结:Service 的分裂人格要分清——started 是任务投递、bound 是能力租赁,两者在 ActiveServices 各记各的账;它不是线程容器,主线程执行一切回调;FGS 是唯一合法的「让用户知道的持续工作」,且从 Android 8 到 16 每年加一道锁(限时转正、禁后台起、强制类型、绑定权限与可见性);IntentService 与各种「bind 保活」都进了退休名单,后台工作的当代答案是 WorkManager 与带类型的 FGS。
参考链接
- https://developer.android.com/guide/components/services
- https://developer.android.com/guide/components/foreground-services
- https://developer.android.com/develop/background/optimized-workmanager(后台工作选型)
- https://developer.android.com/about/versions/14/behavior-changes-all(FGS 类型强制)
- https://cs.android.com/android/platform/frameworks/base/+/main:services/core/java/com/android/server/am/ActiveServices.java
- 站内:广播、ANR、进程模型、架构篇 system_server