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 -->|
    |                   |                   | 处理请求
    | 继续执行          |                   | 无需回复
    |                   |                   |
我的经验:我个人习惯把日志上报、事件通知、心跳检测这类操作设计成 oneway。它们的特点是「丢了就丢了」,不需要保证一定成功。但如果是转账、设置配置这种关键操作,我绝对不会用 oneway。

同步 vs 异步:核心区别

我把它们的区别整理成了一张表,方便你对比:

对比维度 同步调用 异步调用 (oneway)
线程行为 阻塞等待回复 发送后立即返回
返回值 可以有返回值 必须返回 void
异常处理 可以捕获 RemoteException 无法捕获服务端异常
事务队列 一对一,一个请求对应一个回复 可以排队,多个请求共享一个线程
适用场景 需要结果的场景 通知、事件、日志等

oneway 的底层机制

为什么 oneway 能实现异步?这得从 Binder 驱动的事务队列说起。

对于同步调用,Binder 驱动会为每个客户端线程创建一个独立的等待节点。服务端处理完后,驱动通过这个节点找到对应的客户端线程,唤醒它。

对于 oneway 调用,驱动不会创建等待节点。它把事务直接扔给服务端的 todo 队列,然后就完事了。服务端从队列里取出事务,逐个处理。如果服务端忙,事务就在队列里排队。

这里有个坑:oneway 调用虽然不阻塞客户端,但服务端处理不过来时,事务会堆积。我曾经遇到过一个问题——某个 oneway 接口被高频调用,服务端的 todo 队列越积越长,最终导致 Binder 驱动报 FAILED_TRANSACTION 错误。

避坑指南:我曾经在一个项目中,把所有的接口都设计成 oneway,觉得这样性能好。结果服务端处理不过来,事务队列爆了,客户端发请求直接失败。后来我加了一个限流机制,才解决了这个问题。所以,oneway 不是银弹,你得评估服务端的处理能力。

同步调用的超时机制

同步调用还有一个重要特性——超时。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 同步调用 vs 异步调用 (oneway) 流程对比 同步调用 客户端线程 Binder驱动 服务端线程 BC_TRANSACTION BR_TRANSACTION 处理请求 BC_REPLY BR_REPLY ⚠ 客户端线程阻塞等待 ⚠ 有超时机制 (20s) ⚠ 可以有返回值 异步调用 (oneway) 客户端线程 Binder驱动 服务端线程 BC_TRANSACTION BR_TRANSACTION 处理请求 ✓ 客户端立即返回 ✓ 无超时机制 ✓ 必须返回 void ✓ 事务在 todo 队列排队

这张图很直观地展示了两种调用的区别。同步调用是一条线走到底,客户端必须等;异步调用是客户端发完就走,服务端自己慢慢处理。

好了,关于 Binder 的同步与异步,我们就聊到这里。记住一句话:需要结果就用同步,不需要结果就用 oneway。但别忘了评估服务端的处理能力,别让 oneway 的事务把服务端压垮了。

公众号:蓝海资料掘金营,微信 deep3321