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 线程最友好的方案。

Service 与 Activity 通信机制决策树 通信方式选型 Binder / AIDL Messenger BroadcastReceiver 双向调用 高频率数据 串行消息 简单请求 全局通知 生命周期解耦 选型建议 同进程 → Flow / Messenger | 跨进程 → AIDL