16、Binder 的同步与异步:oneway 关键字的作用,同步调用与异步调用的区别
各位同学,今天我们来聊聊 Binder 通信中一个非常关键的设计——同步与异步。说白了,就是客户端发起一个请求后,到底要不要等服务端干完活再回来?
我刚开始接触 Binder 时,觉得这无非就是个阻塞和非阻塞的问题。但后来在项目中踩过坑才发现,这里面的门道比想象中深得多。尤其是那个 oneway 关键字,用好了是神器,用不好就是埋雷。
同步调用:默认的阻塞模式
先看同步调用。这是 Binder 的默认行为。客户端通过 Binder 驱动发送一个 BC_TRANSACTION 事务,然后线程就挂起了。它得等服务端处理完,把结果通过 BC_REPLY 送回来,才能继续往下走。
流程大致是这样的:
客户端线程 Binder驱动 服务端线程
| | |
|--- BC_TRANSACTION -->| |
| |--- BR_TRANSACTION -->|
| | | 处理请求
| |<--- BC_REPLY ------|
|<--- BR_REPLY -----| |
| | |
| 继续执行 | |
这里有个关键点:客户端线程在等待期间是阻塞的。如果服务端处理得慢,客户端这边就卡死了。我在项目中遇到过这种情况——一个后台服务因为数据库锁住了,导致所有调用它的前台界面全部 ANR。嗯,这就是同步调用的代价。
- 客户端发送请求后,线程进入等待状态
- 必须收到回复才能继续执行
- Binder 驱动会维护一个等待队列
- 默认行为,不需要额外声明
异步调用:oneway 关键字
那异步呢?Binder 里用 oneway 关键字来标记。你想想看,如果客户端只是通知服务端做件事,不需要知道结果,那何必傻等呢?
声明方式很简单:
interface IMyService {
// 同步调用
void doHeavyWork(String data);
// 异步调用
oneway void notifyEvent(String event);
// 带回调的异步
oneway void registerCallback(ICallback cb);
}
加了 oneway 后,客户端发完 BC_TRANSACTION 就立刻返回了。它不需要等 BR_REPLY。说白了,这就是个「发完即忘」的模型。
流程变成这样:
客户端线程 Binder驱动 服务端线程
| | |
|--- BC_TRANSACTION -->| |
| 立即返回 |--- BR_TRANSACTION -->|
| | | 处理请求
| 继续执行 | | 无需回复
| | |
同步 vs 异步:核心区别
我把它们的区别整理成了一张表,方便你对比:
| 对比维度 | 同步调用 | 异步调用 (oneway) |
|---|---|---|
| 线程行为 | 阻塞等待回复 | 发送后立即返回 |
| 返回值 | 可以有返回值 | 必须返回 void |
| 异常处理 | 可以捕获 RemoteException | 无法捕获服务端异常 |
| 事务队列 | 一对一,一个请求对应一个回复 | 可以排队,多个请求共享一个线程 |
| 适用场景 | 需要结果的场景 | 通知、事件、日志等 |
oneway 的底层机制
为什么 oneway 能实现异步?这得从 Binder 驱动的事务队列说起。
对于同步调用,Binder 驱动会为每个客户端线程创建一个独立的等待节点。服务端处理完后,驱动通过这个节点找到对应的客户端线程,唤醒它。
对于 oneway 调用,驱动不会创建等待节点。它把事务直接扔给服务端的 todo 队列,然后就完事了。服务端从队列里取出事务,逐个处理。如果服务端忙,事务就在队列里排队。
这里有个坑:oneway 调用虽然不阻塞客户端,但服务端处理不过来时,事务会堆积。我曾经遇到过一个问题——某个 oneway 接口被高频调用,服务端的 todo 队列越积越长,最终导致 Binder 驱动报 FAILED_TRANSACTION 错误。
同步调用的超时机制
同步调用还有一个重要特性——超时。Binder 驱动默认会给同步调用设置一个超时时间。在 Android 中,这个超时时间通常是 20 秒(不同版本可能有差异)。
如果服务端在 20 秒内没有回复,Binder 驱动会发送一个 BR_DEAD_REPLY 给客户端。客户端会收到一个 DeadObjectException。
为什么会这样?因为 Binder 驱动认为,服务端这么久没回复,要么是死了,要么是死锁了。与其让客户端无限等下去,不如直接报错。
我记得有一次排查 ANR 问题,发现就是某个同步调用超时了。服务端因为一个死循环卡住了,客户端等了 20 秒后崩溃。嗯,从那以后,我对同步调用的超时机制格外敏感。
异步调用的线程模型
oneway 调用还有一个特点:服务端处理 oneway 请求的线程,和同步调用是分开的。
在 Binder 线程池中,同步调用和 oneway 调用共享线程池,但 oneway 请求不会占用同步调用的线程资源。具体来说:
- 同步调用:每个请求占用一个线程,直到处理完成
- oneway 调用:多个请求可以排队,由一个线程逐个处理
这意味着,如果你有大量 oneway 请求,它们不会把同步调用的线程池占满。这是 Binder 驱动的一个优化设计。
- 同步调用:阻塞、有返回值、有超时、适合关键操作
- 异步调用:非阻塞、无返回值、无超时、适合通知类操作
- oneway 关键字:声明异步调用的方式,必须返回 void
SVG 流程图:同步 vs 异步调用对比
这张图很直观地展示了两种调用的区别。同步调用是一条线走到底,客户端必须等;异步调用是客户端发完就走,服务端自己慢慢处理。
好了,关于 Binder 的同步与异步,我们就聊到这里。记住一句话:需要结果就用同步,不需要结果就用 oneway。但别忘了评估服务端的处理能力,别让 oneway 的事务把服务端压垮了。