Skip to content

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 拉进绑定方的优先级圈:

  1. bindService → bindServiceLocked:权限与跨用户校验,建账,必要时拉起进程;
  2. scheduleBindService → 应用侧 onBind 返回 IBinder;
  3. ActiveServices.publishService → serviceConnectionLocked → scheduleConnection → 调用者 onServiceConnected;
  4. 解绑 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 保活为什么是幻觉 ​

「绑定一个服务让对方进程不死」听起来像后门,实际上不存在:

  1. adj 传播只保证「对方不低于你」——你自己就是 cached 档时,把对方抬到 cached 没有任何保护力;
  2. 想把自己抬上去(BIND_IMPORTANT 等)需要 android.permission.BIND_... 类特权或系统签名,三方拿不到;
  3. 从最近任务划掉调用方进程组,整组连接一起回收;LMKD 按进程组算账,互相绑定反而一起死;
  4. 系统应用互相绑定(如输入法、无障碍)合法,是因为它们跑在特权域——域转换由 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_bound slice,定位「服务起不来」卡在哪一段。
  • 测试口径: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。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处