Binder(六):服务端方法到底在哪条线程执行?
前五篇已经完成:
调用 → 协议 → 对象 → 驱动 → 服务发现
现在客户端已经拿到目标 Binder 引用,并发起一笔事务。驱动把工作放进服务端进程,但还需要一条用户态线程真正执行:
BR_TRANSACTION
↓
IPCThreadState::executeCommand()
↓
BBinder / JavaBBinder
↓
Stub.onTransact()
↓
服务端业务方法
接下来要分清的是:
服务端哪条线程在驱动里等待事务?同步、oneway、线程池、Handler、嵌套调用和重入又怎样共同决定执行行为?
先用几条边界约束住讨论:
跨进程 Binder 请求通常先由服务端 Binder 线程池中的一条线程执行入口。
同步调用:客户端原线程等待 reply。
oneway:客户端不等待业务 reply,但服务端仍需要 Binder 线程处理。
同一 node 的 oneway 事务串行,但不保证固定在线程池同一线程。
Binder 入口可以再 post 到 Handler;这是服务自身的线程切换策略。
同步嵌套调用会形成事务栈,可能复用调用链中原本等待的线程并产生重入。
源码沿用导读第四节“系列统一基线”,线程语义同时对照 AOSP 官方 Binder threading。
先把线程名字说准确
主线程、Binder 线程与 Handler 线程
| 线程 | 谁创建/管理 | 主要任务 |
|---|---|---|
| 服务端主线程 | 进程运行时 / Looper | 组件生命周期、主 Looper 任务或 system_server 主循环 |
| 服务端 Binder 线程 | Binder 运行时线程池 | 在驱动中等待并执行远端事务入口 |
| 模块 Handler 线程 | 服务代码显式创建或选择 | 串行处理模块内部状态、定时任务和事件 |
这三种线程可能在同一个进程中同时存在,却不是同一条线程。
Binder 负责:把远端事务交给 Binder 入口线程。
Handler 负责:把服务内部任务投递到某个 Looper 线程。
Binder 不会自动把所有服务端方法切到主线程;Handler 也不负责让事务跨进程。
四种常见执行模型
读图时比较两个问题:
客户端线程是否等待 reply?
服务端 Binder 线程是否继续承担业务,还是转交 Handler?
四种情况不能都简称为“异步”:
同步 Binder
AIDL oneway
Binder 入口 post 后立即返回
Binder 入口 post 后同步等待
Binder 线程的创建与等待入口
1. Java App / system_server 路径
Android Java 进程从 Zygote 初始化后,会通过 nativeZygoteInit() 进入 AndroidRuntime::onZygoteInit()。Android 16 的 app_main.cpp 中:
virtual void onZygoteInit() {
sp<ProcessState> proc = ProcessState::self();
proc->startThreadPool();
}
这会启动 Binder 线程池的 main pooled thread。这里的 main 是 libbinder 线程池角色,不是 Java 应用主线程,也不是主 Looper 线程。
2. 普通 Native daemon 路径
Native Binder 服务通常需要显式组织:
ProcessState::self()->startThreadPool();
// 注册 Binder 服务
IPCThreadState::self()->joinThreadPool();
AOSP AIDL backends 也明确提醒:AIDL / Binder 不会因为“发送了一个 Binder 对象”就自动为所有 native 后端启动线程池。
3. servicemanager 是一个特殊例子
Android 16 servicemanager 把最大 Binder 线程数设为 0,并通过 BinderCallback 把 Binder fd 接入 Native Looper:
ps->setThreadPoolMaxThreadCount(0);
sp<Looper> looper = Looper::prepare(false);
BinderCallback::setupTo(looper);
while (true) {
looper->pollAll(-1);
}
这说明“Native 服务端”并不只有一种固定启动模板。所以说讲稳定线程模型时,必须区分普通 libbinder 线程池和把 Binder fd 集成到自有事件循环的特殊服务。
ProcessState 属于进程,IPCThreadState 属于线程
| 对象 | 作用范围 | 主要职责 |
|---|---|---|
ProcessState | 进程级 | 驱动 fd、mmap、handle 代理缓存、线程池配置 |
IPCThreadState | 线程级 | 当前线程输入/输出命令缓冲、transact、调用身份、事务上下文 |
对象关系是:
一个 Binder 进程
└─ 主要 ProcessState
├─ driver fd
├─ Binder mmap / 进程级状态
├─ handle → BpBinder 缓存
└─ 线程池配置
每条参与 Binder 的线程
└─ 自己的 IPCThreadState
├─ mIn / mOut
├─ transact / waitForResponse
├─ calling PID / UID
└─ 当前事务上下文
因此:
ProcessState::self() 讨论进程级 Binder 状态;
IPCThreadState::self() 讨论当前线程怎样与驱动交互。
joinThreadPool() 让当前线程进入 Binder 循环
Android 16 的 IPCThreadState::joinThreadPool() 先向驱动写:
mOut.writeInt32(
isMain
? BC_ENTER_LOOPER
: BC_REGISTER_LOOPER);
随后循环:
talkWithDriver()
↓
读取 BR_* 命令
↓
executeCommand()
↓
处理事务、引用、死亡通知或线程池命令
↓
再次进入驱动等待
所以 Binder 线程不是在 Java 层轮询一个消息列表。它通过当前线程的 IPCThreadState 长期进入驱动,等待 Binder work。
驱动如何请求线程池增长
进程会向驱动设置最大线程数。驱动发现:
没有空闲线程;
没有待响应的 spawn 请求;
已启动线程数小于 max_threads;
就可以向用户态返回:
BR_SPAWN_LOOPER
Android 16 的 IPCThreadState::executeCommand() 收到后调用 ProcessState::spawnPooledThread(false) 创建新池线程。
两个边界要同时记住:
线程按需增长,不代表每笔事务新建线程;
达到上限后,新的事务仍可能排队等待可用 Binder 线程。
线程池繁忙是第九篇排障模型中的重要“目标进程等待执行”阶段。
执行线程与完成语义
同步调用连接两端线程,却不合并线程上下文
以同步 add() 为例:
客户端调用线程
BC_TRANSACTION
↓
waitForResponse()
↓ 等待 BR_REPLY
服务端 Binder 线程
BR_TRANSACTION
↓
Stub.onTransact()
↓
service.add()
↓
BC_REPLY
客户端原线程
BR_REPLY
↓
从 add() 返回
同步返回的准确语义是:
服务端 Binder 方法已经返回;
reply 已回到客户端。
它不必然表示:
服务端 post 到 Handler 的后续工作已经完成;
其他服务的最终状态已经稳定;
所有异步回调已经发出。
oneway 取消 reply 等待,不负责创建线程
oneway 改变的是调用完成语义:
客户端把异步事务交给内核后即可继续;
客户端不等待服务端业务 reply。
它不承诺:
创建新线程;
立即开始执行;
并行执行;
业务一定成功;
远端已经完成状态变更。
服务端仍然需要某条 Binder 线程读取并执行事务。如果线程池繁忙,oneway 也会在服务端排队。
官方线程文档还指出:接收进程可能死亡或冻结而没有处理这笔调用,调用方也未必得到失败反馈。因此不能把 oneway 方法返回写成“服务端执行成功”。
还要避免另一个过度简化:oneway 不等待服务端业务 reply,不等于 transact() 在所有情况下都绝对零等待、绝不失败。Parcel 编码、进入驱动、buffer 压力和本地同进程退化路径仍在调用方路径上;准确说法是异步事务交给内核后,调用线程不再等待远端业务执行完成。
同一 node 的 oneway 保序与线程选择
驱动为每个 binder_node 维护:
has_async_transaction
async_todo
当同一 node 已有 oneway 正在处理时,后续异步事务进入该 node 的 async_todo。上一笔完成释放 buffer 后,驱动再推进下一笔。
因此:
同一 Binder node 的 oneway:不会同时执行;
不同 Binder node 的 oneway:可以由不同线程并行执行。
但“同一 node 串行”不等于:
永远由同一条 Binder 线程执行。
官方 Binder threading:Serialization of asynchronous transactions 明确说明,同一 node 的异步事务可以由线程池不同线程先后执行,只是不同时执行。
所以三个词必须分开:
异步 ≠ 并行
串行 ≠ 固定线程
oneway ≠ 不需要线程安全
把最容易混淆的并发与完成语义压缩成矩阵:
| 事务类型 | 同一 node 是否可同时执行 | 是否固定 Binder 线程 | 调用方是否等待业务 reply |
|---|---|---|---|
| 同步事务 | 条件满足时可以并发 | 否 | 是 |
| 同一 node 的 oneway | 串行,不同时执行 | 否 | 否 |
| 不同 node 的 oneway | 可以并发 | 否 | 否 |
“条件满足”指同时存在多笔调用、目标线程池有可用线程,并且服务实现没有用锁或单线程 Handler 主动串行化。驱动允许并发不等于保证并发。这张表只描述 Binder 入口;服务内部若再转 Handler,还要另外分析 Handler 队列和完成语义。
服务主动 post Handler 才发生线程转交
Binder 线程适合接收并发 IPC 入口,但很多模块希望内部状态只在一条 Looper 线程串行修改:
@Override
public void updateState(State state) {
final int uid = Binder.getCallingUid();
mHandler.post(() -> handleUpdate(uid, state));
}
这里发生两次独立调度:
Binder Driver
→ 选择服务端 Binder 线程执行 IPC 入口
Handler
→ 把内部任务排入目标 MessageQueue
Binder 不知道 Handler 队列;Handler 也不知道远端事务路由。
fire-and-forget 与同步等待 Handler 的完成语义
模式 A:post 后立即返回
public void updateState(State state) {
mHandler.post(() -> applyState(state));
}
时间关系:
Binder 入口完成 post
↓
同步 Binder 方法返回 / oneway 入口结束
↓
Handler 未来某时执行 applyState
客户端看到 Binder 返回,只能证明“投递被入口接受”,不能证明状态已经应用。
模式 B:post 后同步等待
public Result queryState() {
return runWithScissorsOrFutureOnHandler();
}
时间关系:
客户端线程等待 Binder reply
↓
服务端 Binder 线程等待 Handler
↓
目标 Handler 排队或执行
Handler 延迟会沿调用链反向传播成 Binder 慢,最终可能表现为客户端主线程 ANR。
这正是 Binder 第九篇要建立的:
Handler 慢
→ Binder 线程慢
→ 客户端同步调用慢
Messenger 用 Binder 传递 Message,由目标 Handler 串行处理
Messenger 常见模型是:
Binder:跨进程传递 Message
Handler:在目标 Looper 线程串行处理 Message
这里的串行来自目标 Messenger 所绑定的 Handler / Looper,不是 Binder 把所有 Messenger 消息全局串行化。
与 AIDL 的差别主要在业务建模:
AIDL:类型化远程方法;
Messenger:Message 投递协议。
它们底层都可以依赖 Binder,但目标端线程策略不同。Messenger 把 Handler 串行处理直接纳入接口模型;AIDL Stub 默认先运行在 Binder 入口线程,是否转 Handler 由服务实现决定。
调用链折返时:嵌套、重入与死锁
同步嵌套事务形成跨进程调用栈
考虑三笔同步事务:
进程 A,线程 A1
调用进程 B:T1
进程 B,线程 B1 处理 T1
又调用进程 C:T2
进程 C,线程 C1 处理 T2
又回调进程 A:T3
这不是三个完全无关的消息。驱动维护事务之间的父子和等待关系:
T1
└─ T2
└─ T3
当调用链重新进入已经参与事务链的进程 A 时,Binder 可以复用原本正在等待 T1 的线程 A1 来处理 T3,以保持类似本地函数递归的语义。
这就是为什么 Binder 不能只按“请求队列”理解。同步事务还具有调用栈式的嵌套关系。
等待中的线程也可能被同一调用链重入
三个概念必须区分:
并行:多条线程同时执行。
并发:多个调用的执行区间重叠。
重入:前一次调用尚未返回,代码路径再次进入同一对象或状态。
即使进程 A 只有一条 Binder 线程,仍可能:
A1 正在处理本地状态并同步调用 B;
B 处理时同步回调 A;
驱动复用等待中的 A1;
A1 在原调用尚未结束时再次进入同一对象。
如果代码假设“这段状态修改期间绝不会再次进入”,就会出现迭代器失效、状态机越级、重复提交或非递归锁死。
所以:
只有一条 Binder 线程
≠
不存在并发语义和重入风险
持锁跨 Binder 边界会放大等待环
跨进程双锁死锁
进程 A:持有锁 LA,调用 B
进程 B:持有锁 LB,调用 A
两边都等待对方进入自己当前持有的锁,形成跨进程等待环。
单线程重入问题
A1 持有非递归 native mutex 或正在修改状态;
A1 调用 B;
B 回调 A;
回调被重入 A1,再次请求同一 mutex 或访问半完成状态。
AOSP 官方线程文档的直接建议是:
不要在持锁时发起 Binder 调用。
因为跨进程代码、版本和回调方向很难维护全局一致的锁顺序。
典型死锁:Binder 线程等 Handler,Handler 再等 Binder
典型等待环:
App 主线程
同步调用 system_server
等 Binder reply
system_server Binder 线程
post 到 DisplayThread
同步等待结果
DisplayThread
持锁调用另一个 Binder 服务
或等待 App 回调完成
如果 App 回调又需要 App 主线程处理,就可能形成:
App main
→ system_server Binder
→ DisplayThread
→ Binder callback
→ App main
诊断时只看到 App 主线程卡在 transactNative(),不能得出“Binder Driver 慢”。真正瓶颈可能是服务端 Binder 线程等待 Handler,或 Handler 再等待下一笔 Binder。
六个实验 mode 各自证明什么
BinderLab 把各类线程实验拆成独立 mode,避免同一 node 的 oneway backlog 混入 Handler 延迟。每次先 force-stop、清日志,再只启动一个 mode:
$adb = 'D:\Android\platform-tools\adb.exe'
& $adb shell am force-stop com.example.binderdemo
& $adb logcat -c
& $adb shell am start -W `
-n com.example.binderdemo/.MainActivity `
--es experiment oneway-cross-node
六个 mode 如下;每一笔实验事务仍用 requestId 关联。死亡实验另有独立的 binder-death mode,不在本篇线程语义表中:
| mode | 回答的问题 |
|---|---|
handler-latency-baseline | 不注入长阻塞时,同步 Binder 入口等待 Handler 的基线 |
handler-latency-blocked | 注入 1.5 s 队头阻塞后,同一条八点链怎样变化 |
sync-reentry | 同步嵌套 callback 是否在本次运行复用原等待线程 |
oneway-same-node | 同一 ICalculator node 的三笔 async 区间关系 |
oneway-cross-node | N1 与 N2 两个 Binder node 是否存在并发对照 |
async-callback | Binder 入口、Handler 和异步反向 callback 是否分离 |
实验 1:同步 addWithRequestId() 等待 Handler
客户端和服务端都打印:
PID
TID
Thread.currentThread().getName()
进入时间
退出时间
第一篇保留最小 add();本实验用 addWithRequestId(requestId, a, b, injectHandlerBlocker) 关联 C0/S0/H0A/H0B/H1/H2/S1/C1。baseline 与 blocked 使用同一协议、同一 Handler 路径和同一分析器,只改变是否在目标任务前注入 1.5 s blocker。预期:两组均由客户端调用线程等待、服务端 Binder 线程执行并在 reply 后恢复;阻塞组的 H0B→H1 应与基线明显分离。
实验 2:同 node 连续 oneway
calculator.notifyValue(1001, 1);
calculator.notifyValue(1002, 2);
calculator.notifyValue(1003, 3);
耗时必须发生在 Binder 入口内部,不能 post 到单线程 Handler 后再观察 Handler 串行:
@Override
public void notifyValue(int requestId, int value) {
// Lab-only: deliberately blocks a Binder thread.
// Never copy this into production oneway handlers.
long begin = SystemClock.elapsedRealtimeNanos();
log("S_ONEWAY_ENTER", requestId, value, begin, Process.myTid());
SystemClock.sleep(300);
long end = SystemClock.elapsedRealtimeNanos();
log("S_ONEWAY_EXIT", requestId, value, end, Process.myTid());
}
客户端还为每一笔记录 C_SAME_NODE_CALL_BEGIN/RETURN。证据中第二笔调用已经返回时,服务端第一笔仍在执行;三组 ENTER → EXIT 仍不重叠,因此不能再用“第二笔发送得太晚”解释串行。相邻事务的 TID 可以不同;但这组结果单独还不能排除“当时目标进程只有一条活跃 Binder 线程”。若耗时放在同一个 Handler,则更只能证明 MessageQueue 串行,不能证明 Binder 对同一 node 的约束。
实验 3:不同 node 并发对照
服务端创建两个独立 Stub,因此对应两个 Binder node:
interface IAsyncWorker {
oneway void work(int requestId, int value);
}
interface ICalculator {
IAsyncWorker getAsyncWorker(int workerId);
}
客户端先同步取得 N1、N2,再让两条线程同时发送:
线程 A:N1.work(1001, 1) → N1.work(1002, 2)
线程 B:N2.work(1003, 3) → N2.work(1004, 4)
每一笔调用前后分别记录 C_N1_CALL_BEGIN/RETURN 或 C_N2_CALL_BEGIN/RETURN。两边各发两笔是为了让两个 node 都形成 backlog,不构成“每次必然重叠”的保证。强因果闭环要求:
C_N1_CALL_RETURN(1002) < S_N1_EXIT(1001)
C_N2_CALL_RETURN(1004) < S_N2_EXIT(1003)
N1、N2 各自的两段不重叠
N1 与 N2 至少一段重叠,且服务端 TID 不同
由此联合观察到:
目标进程当时确实有至少两条 Binder 线程参与;
同 node 第二笔已经完成客户端 oneway transact 返回;
同 node 的第二笔没有与第一笔并发;
不同 node 的事务可以并发。
客户端 transact 返回不是 Perfetto 里的驱动排队时间戳,但它已经排除 sender 线程直到第一笔结束才调用第二笔的替代解释。不同 node 是“允许并发”,不是“保证每次并发”;某次没有重叠时,应先检查线程池和调度状态,不能反推官方语义错误。
实验 4:Binder 入口转 Handler 与异步 callback
BinderLab 单独提供:
oneway void notifyValueViaHandler(int requestId, int value);
它只在 Binder 入口记录 S_HANDLER_POST,再 worker.post();Handler 执行时记录 S_HANDLER_RUN,并通过 oneway IResultCallback 反向通知 App。分别记录:
Binder 入口返回时间
客户端调用返回时间
Handler 真正执行时间
分析器只把以下稳定偏序作为硬门禁:
C_ASYNC_CALL_BEGIN < C_ASYNC_CALL_RETURN
C_ASYNC_CALL_BEGIN < S_HANDLER_POST < S_HANDLER_RUN < C_CALLBACK < C_ASYNC_CALLBACK_OBSERVED
C_ASYNC_CALL_RETURN < C_ASYNC_CALLBACK_OBSERVED
C_ASYNC_CALL_RETURN 与 post/run/callback 的其他先后受两端并发调度影响;证据日志中 return 早于 post,但这个总顺序不是平台保证。BinderLab 分析器只检查上述稳定偏序,并接受 post 或 callback 早于 return 的合法交错。这条链能证明转线程和角色反转;oneway callback 不参与原同步事务嵌套,也不能证明等待线程复用。
实验 5:同步 callback
使用独立的非 oneway 接口:
interface ISyncResultCallback {
int onResult(int requestId, int value);
}
interface ICalculator {
int addAndCallback(
int requestId,
int a,
int b,
ISyncResultCallback callback);
}
服务端必须在 addAndCallback() 原同步事务内直接调用 callback.onResult(),不能先 post Handler,也不能复用异步 IResultCallback:
App 线程 A1:C_SYNC_CALL_BEGIN(requestId=1001),调用 addAndCallback() 并同步等待
↓
服务端 Binder 线程 B1:S_BEFORE_SYNC_CALLBACK(requestId=1001)
↓
App 进程重新进入:C_SYNC_CALLBACK(requestId=1001)
↓
服务端:S_AFTER_SYNC_CALLBACK(requestId=1001)
↓
App 线程 A1:C_SYNC_CALL_END(requestId=1001)
先确认相同 requestId 的 callback 发生在外层同步调用尚未返回的嵌套区间内,再比较 C_SYNC_CALLBACK.tid 与 C_SYNC_CALL_BEGIN.tid。只有“requestId 一致 + 严格嵌套顺序 + TID 相同”联合成立,才能证明该次运行复用了原等待线程。异步 callback 落在任意空闲 Binder 线程不能支持这个结论。完整实现见 BinderLab。
API 36 实机证据分别覆盖了四组关系:Handler 对照显示 post() 自身很短,注入延迟集中在 H0B → H1,结构化分析满足 blocked.queueNsMin - baseline.queueNsMax ~= injectedBlockerNs;同/跨 node 对照显示两边第二笔均提前提交、node 内串行、不同 Binder TID 上存在重叠;同步重入满足同 requestId、严格嵌套和 TID 相同;async-callback把 Binder 入口、Handler 和反向 callback 分离到不同线程阶段。
这些日志可以排除发送过晚和单 Binder 线程等替代解释,但不是驱动 enqueue 的内部时间戳;一次调度结果也不保证不同 node 每次都并行。
线程答案必须带上调用模式
答案不能只写“Binder 线程”:
1. 跨进程事务先由服务端 Binder 线程或集成 Binder fd 的事件循环接收;
2. 普通 Java AIDL Stub.onTransact() 通常运行在服务端 Binder 线程;
3. 同步调用时客户端原线程等待 reply;
4. oneway 只取消业务 reply 等待,不创建线程,也不保证业务完成;
5. 同一 node 的 oneway 串行,但不固定线程;
6. 服务端可以显式 post 到 Handler,决定入口返回后再做,或让 Binder 线程同步等待;
7. 同步嵌套调用形成事务栈,调用链回到原进程时可能复用等待线程并产生重入;
8. 因此服务端必须同时考虑线程池并行、oneway 串行、Handler 排队和重入。
线程模型到这里闭合,但线程归属还不能回答调用身份。服务端入口虽然运行在自己的 Binder 线程上,当前入站事务仍携带发送方 PID/UID;一旦把工作转给 Handler,这份 calling context 又不会自动跟过去。第七篇从这条身份时间线继续。