先把结论放在前面: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:懒加载的正确实现
下一篇预告:前台服务与后台限制:通知与省电的平衡
有任何问题欢迎在评论区留言交流。