绑定服务与通信

1. Bound Service 是什么

Bound Service 是一种客户端-服务器模式:

Client(Activity/Fragment/其他组件) 绑定 Service
Service 返回 IBinder
Client 通过 IBinder 调用 Service 的能力

它适合:

  • Activity 控制音乐播放 Service。
  • App 内部模块共享一个长期对象。
  • 跨进程调用时暴露 AIDL 接口。
  • 和其他应用组件建立持续连接。

2. 本地绑定:Local Binder

如果 Service 和客户端在同一个进程,最简单方式是自定义 Binder。

class DownloadService : Service() {

    inner class LocalBinder : Binder() {
        fun getService(): DownloadService = this@DownloadService
    }

    private val binder = LocalBinder()

    override fun onBind(intent: Intent): IBinder {
        return binder
    }

    fun startDownload(url: String) {
        // 开始下载
    }

    fun getProgress(): Int {
        return 50
    }
}

Activity 绑定:

class MainActivity : AppCompatActivity() {

    private var downloadService: DownloadService? = null
    private var bound = false

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as DownloadService.LocalBinder
            downloadService = binder.getService()
            bound = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            downloadService = null
            bound = false
        }
    }

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

    override fun onStop() {
        if (bound) {
            unbindService(connection)
            bound = false
        }
        super.onStop()
    }
}

3. 多客户端绑定

多个客户端可以同时绑定同一个 Service。

关键点:

  1. 第一个客户端绑定时,系统调用 onBind()
  2. 系统会缓存 IBinder
  3. 后续客户端绑定时,通常直接拿到同一个 IBinder,不再重复调用 onBind()
  4. 所有客户端都解绑后,系统调用 onUnbind()
  5. 如果 Service 没有 started 状态,则随后销毁。

4. onServiceDisconnected 的含义

onServiceDisconnected() 不是每次 unbindService() 都会调用。

它通常表示连接意外断开,例如:

  • Service 所在进程崩溃。
  • Service 被系统杀死。

正常解绑时,不要依赖它做普通清理,普通清理应该放在调用 unbindService() 附近。

5. start + bind 的常见用法

音乐播放是典型例子:

startService:让音乐离开页面后继续播放
bindService:页面回来后重新控制播放进度、暂停、下一首

启动并绑定:

val intent = Intent(this, MusicService::class.java)
ContextCompat.startForegroundService(this, intent)
bindService(intent, connection, Context.BIND_AUTO_CREATE)

退出页面只解绑:

unbindService(connection)

停止播放时再停止服务:

stopService(Intent(this, MusicService::class.java))

6. 跨进程绑定:AIDL 简述

如果客户端和 Service 不在同一个进程,或者要给其他 App 调用,就不能简单返回本地对象引用,需要使用 IPC。

常见方式:

方式 适合场景
Messenger 简单跨进程消息通信,串行处理
AIDL 复杂跨进程接口,支持多方法调用
Binder 手写接口 系统级或高级场景

AIDL 简化理解:

定义 .aidl 接口 -> 编译生成 Binder Stub/Proxy -> Service 返回 Stub -> 客户端拿 Proxy 调用

7. 绑定服务常见坑

7.1 忘记解绑

bindService() 后必须配对 unbindService()

常见生命周期配对:

绑定位置 解绑位置
onStart() onStop()
onCreate() onDestroy()
用户点击连接 用户点击断开/页面销毁

7.2 在 ServiceConnection 中持有 Activity 导致泄漏

匿名内部类如果长期被 Service 持有,可能间接持有 Activity。一般 App 内部绑定问题不大,但跨进程或复杂连接时要注意清理。

7.3 误解 onServiceDisconnected

正常调用 unbindService() 不一定触发 onServiceDisconnected()

7.4 主线程阻塞

通过 Binder 调 Service 方法时,如果方法内部耗时,依旧可能阻塞调用方线程。不要在 Binder 方法里直接做耗时操作。

8. 面试回答模板

问题:bindService 是怎么通信的?

可以这样回答:

bindService 会让客户端和 Service 建立连接,Service 在 onBind 中返回一个 IBinder。客户端在 ServiceConnection 的 onServiceConnected 中拿到这个 Binder,然后通过 Binder 调用 Service 暴露的方法。同进程可以用自定义 LocalBinder,跨进程一般用 Messenger 或 AIDL。多个客户端可以同时绑定,系统通常只在第一个客户端绑定时调用 onBind 并缓存 IBinder,所有客户端解绑后,如果 Service 没有被 start,就会销毁。