34. 同步与异步:Binder 的同步调用与异步调用(oneway)的区别

Binder 的同步和异步,说白了就是「等不等结果」的问题。

我刚开始接触 Binder 时,觉得这俩不就是加个 oneway 关键字嘛,有啥好讲的?后来在项目里踩了坑,才发现这里面的门道比想象中深得多。

今天咱们就好好掰扯掰扯,同步调用和异步调用到底差在哪,什么时候该用哪个。

同步调用:你发完请求,必须等回复

同步调用是 Binder 的默认行为。客户端发起一个请求后,会阻塞在 transact() 调用上,直到服务端处理完并返回结果,客户端才能继续往下走。

我打个比方:你去柜台办业务,把材料递进去,然后站在窗口等着。柜员处理完了,把回执单递给你,你才离开。这就是同步。

代码上看,同步调用长这样:

// 客户端代码
IBinder binder = serviceManager.getService("my_service");
IMyService service = IMyService.Stub.asInterface(binder);

// 这行会阻塞,直到服务端返回
int result = service.doSomething(input);
// 只有拿到 result 后,才会执行下一行
Log.d("TAG", "result = " + result);

同步调用的特点很明显:

  • 简单直观:写起来就像普通函数调用,不需要回调
  • 天然保证顺序:前一个请求处理完,后一个才开始
  • 会阻塞线程:如果服务端处理慢,客户端线程就卡住了
注意:同步调用不能在 UI 线程执行。我在项目中见过有人直接在 Activity 的 onCreate 里调 Binder 同步接口,结果界面卡死几秒钟,直接被 ANR 干掉。嗯,血的教训。

异步调用(oneway):发完就走,不等回复

异步调用,就是在 AIDL 接口方法前加上 oneway 关键字。客户端调用后立即返回,不等待服务端处理结果。

还是那个柜台的例子:你把材料递进去,柜员说「好了你走吧,办完了通知你」。你扭头就走,该干嘛干嘛。这就是异步。

代码上这样写:

// AIDL 定义
oneway interface IMyService {
    void doSomethingAsync(in String input);
    // 注意:oneway 方法不能有返回值,也不能有 out 参数
}

// 客户端调用
IMyService service = IMyService.Stub.asInterface(binder);
service.doSomethingAsync(input);  // 立即返回,不阻塞
// 这里立刻执行,不等服务端处理完
Log.d("TAG", "请求已发出");

异步调用的特点:

  • 不阻塞线程:调用立即返回,客户端可以继续做其他事
  • 没有返回值:因为不等结果,所以方法必须是 void 类型
  • 参数有限制:只能有 in 参数,不能有 outinout
我的经验:oneway 调用虽然不阻塞客户端,但 Binder 驱动内部仍然会排队处理。如果服务端处理不过来,请求会积压在驱动层的队列里。我曾经遇到过 oneway 调用丢消息的情况——队列满了,新请求直接被丢弃。所以别以为 oneway 就万事大吉了。

核心区别对比

对比维度 同步调用 异步调用(oneway)
调用是否阻塞 阻塞,直到服务端返回 立即返回,不阻塞
返回值 可以有返回值 必须是 void
参数方向 in / out / inout 都可以 只能 in
调用线程 不能是 UI 线程 可以在 UI 线程
可靠性 高,有明确的结果 低,可能丢消息
典型场景 查询数据、获取状态 通知、事件上报、日志

底层原理:Binder 驱动怎么处理?

你想想看,同步和异步在 Binder 驱动层面是怎么实现的?

其实核心区别在于 BC_TRANSACTIONBC_REPLY 这两个命令的处理方式。

同步调用时:

  • 客户端发送 BC_TRANSACTION,然后进入等待状态
  • 服务端处理完后,发送 BC_REPLY 回来
  • 驱动唤醒客户端,把结果传回去

异步调用时:

  • 客户端发送 BC_TRANSACTION,但 transaction_flags 中设置了 TF_ONE_WAY
  • 驱动收到后,直接把请求扔给服务端,不等待回复
  • 客户端线程立即返回,继续执行

说白了,同步调用是「一问一答」,异步调用是「只问不答」。

关键点:异步调用在驱动层面不会创建 BC_REPLY 事务。这意味着服务端即使崩溃了,客户端也感知不到。我曾经调试过一个 bug,服务端进程被 kill 了,客户端还傻乎乎地继续发 oneway 请求,数据全丢了。所以用 oneway 时,一定要做好「消息丢了怎么办」的预案。

什么时候用同步,什么时候用异步?

我个人习惯这样判断:

用同步的场景:

  • 你需要拿到结果才能继续
  • 操作必须保证执行完成
  • 调用频率不高,服务端处理快

用异步的场景:

  • 你只是通知一下,不关心结果
  • 调用频率很高,不想阻塞客户端
  • 数据可以丢,丢了也无所谓

举个例子:

  • 获取联系人列表 → 同步,因为你要展示给用户
  • 上报用户点击事件 → 异步,丢了就丢了,不影响功能
  • 设置系统音量 → 同步,要确保设置成功
  • 发送心跳包 → 异步,频繁调用,不阻塞主流程

SVG 流程图:同步 vs 异步调用流程

Binder 同步调用 vs 异步调用流程 客户端线程 Binder 驱动 服务端线程 同步调用 BC_TRANSACTION 转发请求 处理中... BC_REPLY 返回结果 线程阻塞等待 异步调用 (oneway) BC_TRANSACTION (TF_ONE_WAY) 转发请求 处理中... 立即返回,继续执行 没有 BC_REPLY,服务端处理结果客户端无法感知

避坑指南

我曾经在做一个日志上报模块时,把所有接口都设计成 oneway,觉得这样性能好。结果上线后发现,某些关键日志丢了,排查了半天才发现是驱动队列满了。

后来我学乖了:

  • 关键数据用同步,确保送达
  • 非关键数据用异步,但加上本地缓存和重试机制
  • 高频调用一定要控制频率,别把驱动队列撑爆
另一个坑:oneway 方法不能有返回值,但很多人不知道的是,它也不能抛异常。如果你在 oneway 方法里写了 throws,编译能过,但运行时异常永远不会传到客户端。我见过有人用 oneway 方法做权限校验,结果权限不足时服务端抛了异常,客户端完全不知道,还以为调用成功了。嗯,这种 bug 特别难查。

总结一下:同步和异步没有绝对的好坏,关键看场景。同步保可靠,异步保性能。选对了,系统稳如老狗;选错了,bug 追到秃头。


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