第128篇Service 生命周期:started 与 bound 两条线

简介: Service本质无“运行中/已停止”状态,仅分启动模式(startService)与绑定模式(bindService)。前者靠onStartCommand响应,支持START_STICKY自动重启;后者依赖onBind/onUnbind,绑定全解即销毁。核心原则:不操作UI、不耗时、不持Activity引用,状态交由UI层订阅。

先把结论放在前面:Service 没有"运行中/已停止"这种进程内状态机,它只回答两个问题——"你是启动模式还是绑定模式"和"系统是否还欠你一个实例"。 bindService 走 onCreate → onBind → onServiceConnected,所有绑定都解除即销毁;startService 走 onCreate → onStartCommand,返回 START_STICKY 后系统会在进程被杀后重新创建并补发 null Intent。这两条能立住,后面的线程、进程、后台限制就都能挂上。

Service 生命周期这题的主角是 Service:它是什么、底层怎么运转、真实项目里长什么样,层层往下讲。

两条创建路径,四种回调组合

Service 生命周期方法是 onCreate、onStartCommand、onBind、onUnbind、onDestroy 五个。实际项目里只会用到其中两套组合。

启动模式(startService / startForegroundService):

class SyncService : Service() {
   

    override fun onBind(intent: Intent?): IBinder? = null   // 不参与绑定

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
   
        // intent 为 null 表示系统重启后补发,需要按"重新同步"处理而不是当普通请求
        val trigger = intent?.action ?: ACTION_SYNC_NOW
        scope.launch {
    syncRepository(trigger) }
        return START_STICKY   // 或 START_NOT_STICKY / START_REDELIVER_INTENT
    }

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    override fun onDestroy() {
   
        scope.cancel()      // 不取消 = 协程持有 Service = 泄漏
        super.onDestroy()
    }

    companion object {
   
        const val ACTION_SYNC_NOW = "com.demo.sync.NOW"
    }
}

绑定模式(bindService): onBind 返回 IBinder,客户端拿到后可以直接调方法(进程内)或走 AIDL(跨进程)。

class MusicService : Service() {
   

    private val binder = LocalBinder()

    inner class LocalBinder : Binder() {
   
        fun service(): MusicService = this@MusicService
    }

    override fun onBind(intent: Intent): IBinder = binder

    override fun onUnbind(intent: Intent?): Boolean {
   
        releasePlayer()
        return true          // 允许 onRebind 被再次调用
    }
}

// 客户端
private val connection = object : ServiceConnection {
   
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
   
        val music = (service as MusicService.LocalBinder).service()
        music.play(list)
    }
    override fun onServiceDisconnected(name: ComponentName?) {
    /* 进程被杀 */ }
}

override fun onStart() {
   
    super.onStart()
    bindService(Intent(this, MusicService::class.java), connection, Context.BIND_AUTO_CREATE)
}

override fun onStop() {
   
    super.onStop()
    unbindService(connection)   // 少一次 = 泄漏
}

绑定模式的两个关键性质(答出来是分水岭):① onCreate 在首次 bind 且 BIND_AUTO_CREATE 时调用;② 只要还有一个绑定没解,Service 就不会被销毁——这解释了"Activity 退后台但音乐还在放"的实现原理。

最常见的坑是

在 Service 里直接更新 UI,跨线程与跨生命周期双双违规。

Service 的回调跑在主线程,但它没有窗口(startService 启动时没有对应的 Activity 栈),所以往屏幕上贴 UI 既不知道挂到哪,也会在 Activity 已销毁时操作旧视图。表现是 WindowManager$BadTokenException,或者界面已经不在了但 View 还在刷新导致内存泄漏。

正确做法是让 Service 只产出状态,由 UI 层订阅:

class PlaybackService : Service() {
   

    private val _state = MutableStateFlow<PlaybackState>(PlaybackState.Idle)
    val state: StateFlow<PlaybackState> = _state.asStateFlow()   // 对外只读

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
   
        scope.launch {
   
            _state.value = PlaybackState.Playing
            // 状态变化从这里流出,UI 自己决定怎么显示
        }
        return START_NOT_STICKY
    }
}

UI 侧按生命周期订阅:

class PlayerActivity : AppCompatActivity() {
   

    private val service: PlaybackService by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
   
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
   
            repeatOnLifecycle(Lifecycle.State.STARTED) {
   
                service.state.collect {
    state -> render(state) }
            }
        }
    }
}

顺带三个必答的细节:① onStartCommand 的 startId 与 flags 用于处理"多次 start 合并投递"(flags & START_FLAG_REDELIVERY 表示 Intent 被重投);② onTaskRemoved 决定用户从最近任务划掉后 Service 是否继续(START_STICKY 配合它可以决定要不要重启);③ onDestroy 里必须清理资源——协程 cancel()、播放器 release()、注册的监听与回调移除,否则 Service 被销毁后仍有对象引用它,泄漏链一路挂到 Application。

还有一个更隐蔽的坑:Service 生命周期的实践经验是前提先声明,失败路径给兜底,事故挡在上线前。落到具体动作:① 显式声明 android:exported 与权限,别让外部能随手 bind;② 跨进程调用要判空并降级(onServiceDisconnected 里恢复重连);③ 关键任务不要只依赖 Service,进程被杀后靠 WorkManager 兜底重投。

还有两个话题是这一题常被追问、但很少有人主动讲的。

话题一:Service 所在的进程优先级。 Service 和它的宿主 Activity 跑在同一个进程里,所以进程被杀的概率取决于"这个进程里有没有前台界面"。典型问题是:一个应用有主界面(前台)和播放服务(Service),用户回桌面后进程优先级下降、Service 被杀,播放中断,而用户界面上没有任何提示。解法有两条——开前台服务(播放类服务本来就该是前台的),或者把 Service 放到独立进程(android:process=":playback"),这样它与 UI 进程的生命周期解耦,一个被杀不影响另一个。独立进程的代价是跨进程通信(Binder)开销与数据序列化成本,所以只在确有隔离需求时用。

话题二:进程内绑定与 AIDL 怎么选。 判断标准是"要不要跨进程"。同进程只需要传数据或调方法时,用 Binder 子类 + 类型强转即可,零序列化开销;需要跨进程(比如服务给另一个应用用、或者要独立进程)时,才上 AIDL——定义接口、编译生成 Stub/Proxy、实现 onBind 返回 Stub.asInterface()。AIDL 的两个必答题:Binder 事务也有大小限制(约 1MB,跨进程传大对象同样会 TransactionTooLargeException),以及跨进程调用是同步阻塞的,所以接口里的方法要在子线程执行。

// AIDL 接口(跨进程时)
interface IMusicService : android.os.IInterface {
   
    fun play(path: String?)      // 简单类型可以直接传
    fun loadList(items: List<Parcelable>?)   // 自定义对象需 Parcelable
}

顺带一个工程建议:能不跨进程就不跨进程。 独立进程带来的 Context 重复初始化、静态状态不共享、崩溃隔离面变大等问题,代价往往被低估。真要隔离,优先考虑"多模块 + 明确边界"的架构,而不是多进程。

三条高频追问

问:startService 和 bindService 能同时用吗?

可以,生命周期是并集:先 bind 会触发 onCreate(不触发 onStartCommand),再 start 会触发 onStartCommand;unbind 时 Service 仍在(因为还有 start 撑着),stopSelf 后才 onDestroy。这正是"音乐服务既支持后台播放又提供绑定控制"的实现方式。但要清楚 stopSelf 与 unbind 谁先到会决定行为,所以要显式 stopSelf() 而不是靠系统回收。

问:START_STICKY 与 START_REDELIVER_INTENT 的区别?

START_STICKY 在进程被杀后重启 Service,但 onStartCommand 收到 null Intent,适合"周期性重新拉起"的场景;START_REDELIVER_INTENT 会把最后那个 Intent 重新投递,适合"上次请求还没做完"的任务。START_NOT_STICKY 则不重启。选错的代价很实际:同步任务用 STICKY 会丢参数,播放服务用 REDELIVER 会拿到过期的播放列表。

问:Service 跑在哪个线程?

主线程,且与 Activity 共享进程与主线程 Looper。所以耗时工作必须自己开线程/协程。Service 被杀不等于进程被杀——进程里可能还有 Activity 在跑;反过来进程被杀则所有 Service 状态丢失。区分这两点是这一题的核心分水岭。

问:为什么 Android 8 之后 startService 会抛异常?

后台执行限制:应用处于后台时调用 startService 会被系统拒绝并抛 IllegalStateException("Not allowed to start service Intent")。替代方案是用 startForegroundService 并在 5 秒内调 startForeground 通知,否则抛 ForegroundServiceDidNotStartInTimeException。这条规则是后面一篇「前台服务与后台限制」的主题。

落到项目里怎么做

一条能直接落地的规则:Service 不碰 UI、不做耗时、不持有 Activity 引用。它的职责只有"接收命令、产出状态、管理资源生命周期"三件事。

配套实践三条:① 状态用 StateFlow 暴露给 UI,UI 用 repeatOnLifecycle 订阅;② 所有协程、监听、播放器在 onDestroy 清理,onTaskRemoved 里按业务需要 stopSelf();③ 需要"进程死亡也要完成"的任务一律走 WorkManager,Service 只做即时响应——这两者的分工讲清楚,面试就基本拿下了。

给正在准备面试的你

Service 生命周期的答法要立住"Service 没有运行态,只有创建与销毁"。推荐这条线:先分启动/绑定两条路径 → 讲清 bindService 走 onCreate→onBind→onServiceConnected、绑定全解即销毁 → 讲 startService 走 onCreate→onStartCommand、START_STICKY 补发 null Intent → 由此推出后台限制与前台服务 → 落"不碰 UI、不做耗时、不持有 Activity 引用"的工程规则。补上"Service 被杀 ≠ 进程被杀"这个区分,答案就完整了。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:ViewPager2-与-Fragment:懒加载的正确实现

下一篇预告:前台服务与后台限制:通知与省电的平衡

有任何问题欢迎在评论区留言交流。

相关文章
|
18天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8618 25
|
16天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
3040 14
|
16天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2110 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
5天前
|
人工智能 JSON Linux
【全网最详细】ComfyUI使用教程:下载+本地部署+配置+工作流搭建一篇搞定(2026最新版)
ComfyUI是一款免费开源的本地AI绘图工具,采用节点式工作流设计,支持文生图、图生图、局部重绘、放大、换脸等多种功能。可离线运行,依赖显卡加速,无需联网。支持自定义流程保存与分享,插件生态丰富,适合进阶用户。(239字)
|
16天前
|
云安全 人工智能 安全
|
11天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
11天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)

热门文章

最新文章