Service与Activity通信机制归纳总结
前台服务跑起来之后,怎么跟 Activity 打招呼?说白了就是两个进程(或两个组件)之间怎么传数据、发指令。尤其是当 Compose 界面正在高频刷新,你总不能让通信过程去抢主线程的锁吧?我早期做 IM 应用时,就因为 Binder 回调阻塞了 UI 线程,导致帧率直接掉到 30fps……后来学乖了,通信方案得选对,还得用对。
核心原则:避免在 Activity 的主线程中执行耗时任务,所有跨进程/跨线程通信都必须考虑生命周期与线程安全。
主流通信方式一览
| 方式 | 适用场景 | 线程安全 | 个人评价 |
|---|---|---|---|
| Binder (AIDL) | 高频率数据交换、双向调用 | 需手动处理 | 功能最强,但容易踩坑 |
| Messenger | 串行消息、单向请求 | 内置 Handler 队列 | 简单可靠,适合轻量通信 |
| BroadcastReceiver | 全局通知、一次性事件 | 系统序列化 | 耦合低,但效率一般 |
| 共享文件/数据库 | 大量结构化数据 | 需锁机制 | 我一般不推荐,太原始 |
首选方案:Binder + AIDL
Android 里最正统的 IPC 方式。Service 暴露一个接口,Activity 通过 bindService 拿到 Binder 对象,然后直接调方法。注意:Binder 的默认线程模型是 Binder 线程池,回调到 Activity 端时仍可能跑到 Binder 线程里,所以更新 UI 必须切回主线程。
// IMyService.aidl
interface IMyService {
void sendData(String data);
String getLatestStatus();
}
// Service 端实现
class MyService : Service() {
private val binder = object : IMyService.Stub() {
override fun sendData(data: String?) {
// 这里在 Binder 线程中运行
Log.d("Service", "收到数据: $data")
}
override fun getLatestStatus(): String {
return "running"
}
}
override fun onBind(intent: Intent?): IBinder = binder
}
// Activity 端绑定
private var service: IMyService? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
service = IMyService.Stub.asInterface(binder)
// 记得用 Handler 切回主线程
handler.post { textView.text = service?.latestStatus }
}
}
我的小技巧:如果回调需要频繁更新 Compose 的 State,建议在 Activity 内用 MutableStateFlow 接收数据,然后通过 collectAsState 驱动 Compose 重组。这样既避免了线程问题,也减少了不必要的 recomposition。
进阶玩法:使用 LiveData / Flow 包装
直接裸用 AIDL 回调太原始。我习惯在 Service 内维护一个 SharedFlow,Activity 绑定后 collect 它的数据流。这样你甚至不需要写 AIDL 回调接口——Service 自己把数据 push 到 Flow 里,Activity 端观察就行。但注意,Flow 本身不能跨进程传递,所以这个方案仅适用于同一个进程内的 Service(前台服务默认与 Activity 同进程,除非你特意指定)。
// Service 内
class MyService : Service() {
private val _statusFlow = MutableStateFlow("idle")
val statusFlow: StateFlow<String> = _statusFlow.asStateFlow()
fun updateStatus(newStatus: String) {
_statusFlow.value = newStatus
}
}
// Activity 内通过绑定拿到 Service 实例(仅同进程)
val serviceRef = (service as? LocalBinder)?.getService()
lifecycleScope.launch {
serviceRef?.statusFlow?.collect { status ->
// 自动在主线程,更新 Compose State
}
}
轻量选择:Messenger
如果只是传个简单的消息,不想写 AIDL 文件,Messenger 是很好的替代品。它基于 Handler 实现,天然线程安全。我在做后台下载进度推送时就用它,代码量少,也不容易出错。
// Service 端
class MyService : Service() {
private val messenger = Messenger(Handler { msg ->
when (msg.what) {
1 -> Log.d("Service", "收到消息: ${msg.data.getString("key")}")
}
true
})
override fun onBind(intent: Intent?) = messenger.binder
}
// Activity 端
private val replyHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// 这里已经是主线程
textView.text = msg.data.getString("result")
}
}
private val replyMessenger = Messenger(replyHandler)
// 发送消息
val msg = Message.obtain().apply {
what = 1
data = Bundle().apply { putString("key", "hello") }
replyTo = replyMessenger // 双向通信
}
serviceMessenger?.send(msg)
避坑指南(我踩过的雷)
- 别在 Binder 回调里直接更新 UI:我曾经直接在
IMyService.Stub里调textView.text,结果闪退 + 卡顿。一定要 post 到主线程。 - 绑定和解绑时机:Activity 的
onStart里绑定,onStop里解绑。如果使用 ViewModel 管理,可以把绑定逻辑放在 ViewModel 里,避免 Activity 重建重复绑定。 - 前台服务优先级高,但不能滥用:如果你只是传个数据,用普通 Service + Activity 绑定就够了。非必要不启动前台服务,否则会加剧 Compose 帧率竞争。
重要提醒:当 Service 与 Activity 之间频繁传递大数据(如视频帧、大 JSON)时,Binder 缓冲区有 1MB 限制。超了会崩溃。建议使用
MMKV 或共享内存方式传递大块数据。
好了,聊了这么多,总结一句话:通信方式没有银弹。同进程且简单场景用 Messenger,复杂双向用 AIDL,想解耦就用 Flow。你现在要做的是根据前台服务的实际负载,选一个对 UI 线程最友好的方案。