【全栈第1课】Binder — Android 跨进程通信的命脉
Android 全栈工程师进阶教程 第01课。Binder — Android 跨进程通信的命脉。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 01 课。完整 26 课见 GitHub 仓库。
【全栈第1课】Binder — Android 跨进程通信的命脉
0. 这节课你将亲手做出什么
一个真实的 AOSP Binder 服务:写一个 IHelloService 接口(有 sayHello、add 两个方法),编译出 helloserver(服务端)和 helloclient(客户端)两个可执行文件。
跑起来后,helloclient 进程能跨进程调到 helloserver 进程里的方法,并且服务端能拿到调用方的 uid(内核盖的戳)。
学完你能回答:你天天写的 context.getSystemService(...)、Binder.getCallingUid(),底层到底发生了什么?
1. 背景:为什么 Android 不用传统 IPC,要自己造个 Binder
Android 是多进程系统:你的 App 一个进程,系统服务(AMS/WMS/IMS)在 system_server 进程。它们要通信,就需要 IPC(进程间通信)。Linux 本来就有一堆 IPC,为什么 Google 还要造 Binder?
| IPC 方式 | 数据拷贝次数 | 致命问题 |
|---|---|---|
| 管道 / 消息队列 | 2 次(发送方→内核缓冲→接收方) | 慢 |
| Socket | 2 次 | 慢,主要为网络设计 |
| 共享内存 | 0 次 | 快,但谁都能读写,没有身份认证 |
| Binder | 1 次(靠 mmap) | 快 + 内核给每次调用盖上 UID/PID 戳 |
Binder 的两个杀手锏:
- 一次拷贝:发送方的数据
copy_from_user到内核空间,而接收进程通过mmap把这块内核缓冲区直接映射到自己的地址空间——接收方读的时候不用再拷一次。所以是 1 次,比管道少一半。 - 内核盖身份戳:每次跨进程调用,Binder 驱动会从内核的进程信息里取出真实的调用方 uid/pid,填进这次调用。调用方伪造不了——因为这是内核填的,不是应用层传的参数。
> 💡 这第 2 点就是你在 示例项目 里写 Binder.getCallingUid() 做权限校验为什么安全的根本原因:应用层骗不过内核。
2. 一次跨进程调用的全链路(先建立全局图)
以你最熟的 inputManager.getInputDevice() 为例:
【你的 App 进程】 【system_server 进程】
inputManager.getInputDevice(id)
│
▼ IInputManager.Stub.Proxy ← ① AIDL 自动生成的"客户端代理"
.getInputDevice(id)
│ 把参数 id 打包进 Parcel
▼ mRemote.transact(code, data, reply)
│
▼ BinderProxy.transactNative() (JNI)
│
╞═══════ Binder 驱动(内核)═══════▶ 唤醒 system_server 的 binder 线程
│ (盖上调用方 uid/pid) │
│ ▼ IInputManager.Stub.onTransact(code) ← ② "服务端骨架"
│ │ 按 code 解包 Parcel,分发
│ ▼ InputManagerService.getInputDevice() ← ③ 真正的实现
│ │ 结果打包进 reply
◀═══════ Binder 驱动 ═══════════════════
▼
拿到 reply,解包,返回 InputDevice
记住三个角色(整个 Binder 体系就这三个):
| 角色 | 在哪一端 | 干什么 | 谁写的 |
|---|---|---|---|
| Proxy(代理) | 客户端 | 打包参数 + 发起 transact | AIDL 工具自动生成 |
| Stub(骨架) | 服务端 | onTransact 收包 + 解包 + 分发 | AIDL 工具自动生成 |
| Service(实现) | 服务端 | 真正的业务逻辑 | 你写 |
这就是为什么用 AIDL:你只写接口和实现,打包/解包/transact 这些脏活全自动生成。
3. 在 某真实机型 真实源码里印证(不是教科书,是量产代码)
3.1 身份戳从哪来 —— getCallingUid 的真相
你调 Binder.getCallingUid() 时:
Java: Binder.getCallingUid()
│ JNI
▼ android_os_Binder_getCallingUid() frameworks/base/core/jni/android_util_Binder.cpp:1234
│
▼ IPCThreadState::self()->getCallingUid() 返回 mCallingUid
frameworks/native/libs/binder/IPCThreadState.cpp:445
mCallingUid 这个值,是 Binder 驱动在每次 transact 时从内核填进来的。还有个配套的 clearCallingIdentity()(IPCThreadState.cpp:547)—— 系统服务里"临时变回自己身份去做特权操作"就用它,做完再 restoreCallingIdentity()。这个模式你在系统服务源码里会反复见到。
3.2 服务怎么"上架" —— IMS 的注册
InputManagerService.Lifecycle.onStart() InputManagerService.java:4312
└─ publishBinderService(Context.INPUT_SERVICE, mService)
└─ ServiceManager.addService("input", mService, ...) SystemService.java:663
而 App 端 getSystemService(INPUT_SERVICE) 最终走到 ServiceManager.getService("input"),拿到的就是 IMS 的 BinderProxy。
addService 上架 ↔ getService 取货——ServiceManager 就是系统服务的"黄页/114"。它自己也是个 Binder 服务,handle 固定为 0。
4. 动手:一步步做出来
步骤 1:定义接口 aidl/com/example/hello/IHelloService.aidl
package com.example.hello;
interface IHelloService {
// @utf8InCpp 让生成的 C++ 用 std::string(否则是 String16,麻烦)
@utf8InCpp String sayHello(@utf8InCpp String name);
int add(int a, int b);
}
你只写这一个文件,aidl 工具会自动生成 4 个产物:
IHelloService.h— 接口BnHelloService.h— 服务端骨架(Stub),自带onTransactBpHelloService.h— 客户端代理(Proxy),自带打包 transactIHelloService.cpp— 上面的实现
步骤 2:写服务端 helloserver.cpp(逐段讲)
头文件和命名(注意第一行 include 的是生成的 Bn 头):
#include <binder/IPCThreadState.h> // 取调用方 uid/pid
#include <binder/IServiceManager.h> // defaultServiceManager / addService
#include <binder/ProcessState.h> // binder 线程池
#include "com/example/hello/BnHelloService.h" // ← aidl 自动生成的骨架
using com::example::hello::BnHelloService;
实现类:继承生成的 Bn(骨架),只写业务方法:
class HelloService : public BnHelloService { // 继承 Stub
public:
Status sayHello(const std::string& name, std::string* _aidl_return) override {
// ★ 关键:这里取到的 uid 是内核盖的,客户端伪造不了
const int uid = IPCThreadState::self()->getCallingUid();
const int pid = IPCThreadState::self()->getCallingPid();
ALOGI("sayHello called by uid=%d pid=%d, name=%s", uid, pid, name.c_str());
*_aidl_return = "Hello, " + name + " (from uid=" + std::to_string(uid) + ")";
return Status::ok(); // AIDL 方法用 Status 返回成功/失败
}
Status add(int32_t a, int32_t b, int32_t* _aidl_return) override {
*_aidl_return = a + b; // 返回值通过出参指针 _aidl_return 带回
return Status::ok();
}
};
> 注意:AIDL 生成的 C++ 方法,返回值不是函数返回值,而是最后一个出参 _aidl_return,函数本身返回 Status(表示这次调用成功还是失败)。这是 AIDL C++ 后端的固定约定。
> onTransact(解包+按 code 分发)由 BnHelloService 生成代码处理,你完全不用碰。
main:创建实例 → 上架 → 开线程池阻塞等调用:
int main() {
sp<HelloService> service = sp<HelloService>::make(); // sp<> 是 Android 的强引用智能指针
// 上架到 servicemanager,名字 "hello"
status_t ret = defaultServiceManager()->addService(String16("hello"), service);
if (ret != OK) { LOG(ERROR) << "addService failed!"; return 1; }
ProcessState::self()->startThreadPool(); // 开启 binder 线程池
IPCThreadState::self()->joinThreadPool(); // 阻塞,把当前线程也加入,等 transact 进来
return 0;
}
> 为什么要 joinThreadPool 阻塞? 因为服务端是被动的——没人调用时它就睡着(epoll 阻塞在 binder fd 上,0 CPU),有 transact 进来才被驱动唤醒。不阻塞的话 main 一返回进程就退了,服务就没了。
步骤 3:客户端 helloclient.cpp 关键逻辑
sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> binder = sm->getService(String16("hello")); // 取货:拿到 BinderProxy
sp<IHelloService> svc = interface_cast<IHelloService>(binder); // 转成接口代理(Bp)
std::string reply;
svc->sayHello("zhoubenliang", &reply); // 看着像本地调用,实际跨进程了
ALOGI("got: %s", reply.c_str());
步骤 4:Android.bp 编译脚本
aidl_interface {
name: "com.example.hello",
unstable: true, // 学习用,不冻结版本
srcs: ["aidl/com/example/hello/IHelloService.aidl"],
backend: { cpp: { enabled: true }, java: { enabled: true } }, // 生成 C++ + Java 后端
}
cc_binary {
name: "helloserver",
srcs: ["helloserver.cpp"],
shared_libs: ["libbinder", "libutils", "liblog", "libbase"],
static_libs: ["com.example.hello-cpp"], // 链接 aidl 生成的 C++ 库
}
// helloclient 同理
5. 看一眼"自动生成"到底生成了什么(理解 transact 的关键)
用 aidl 工具生成后,BpHelloService::sayHello(客户端代理)真实长这样:
::android::binder::Status BpHelloService::sayHello(const std::string& name, std::string* _aidl_return) {
::android::Parcel _aidl_data, _aidl_reply;
_aidl_data.writeInterfaceToken(getInterfaceDescriptor()); // 写接口标识,防串台
_aidl_data.writeUtf8AsUtf16(name); // ① 打包参数 name
remote()->transact(BnHelloService::TRANSACTION_sayHello, // ② 发起 transact,过 binder 驱动!
_aidl_data, &_aidl_reply, 0);
_aidl_status.readFromParcel(_aidl_reply); // ③ 解包返回值
...
}
这就是第 2 节链路图里"Proxy 打包→transact"的真实代码。 你写一行 .aidl,这一坨样板全自动生成。这就是 AIDL 的价值。
6. 编译 & 运行(看到效果)
# AOSP 根目录 source/lunch 之后,进 lesson 目录:
mm # 编译,产出 helloserver / helloclient
adb push out/.../helloserver /data/local/tmp/
adb push out/.../helloclient /data/local/tmp/
adb shell /data/local/tmp/helloserver & # 后台起服务端
adb shell /data/local/tmp/helloclient zhoubenliang # 客户端调用
adb logcat | grep -i hello # 看完整链路日志
预期 logcat(亲眼看到 Binder 生效 + 身份戳):
helloclient: >>> 发起 sayHello("zhoubenliang") 跨进程调用
helloserver: sayHello called by uid=2000 pid=12345 ← 服务端收到,uid 是客户端的(shell=2000)
helloclient: <<< 收到返回: Hello, zhoubenliang (from uid=2000)
那个 uid=2000 就是内核盖的戳——服务端没法被客户端骗,这一刻你就理解了权限校验的根基。
7. 踩坑提醒
- AIDL C++ 返回值是出参,不是函数返回值,函数返回
Status。新手常写成return result;→ 编不过。 - 服务端必须
joinThreadPool阻塞,否则 main 返回进程退出,服务消失。 @VintfStability不要乱加(那是 HAL 用的,见 L9),这里unstable: true即可。- 这一课的 helloserver 是独立 cc_binary —— 能编进镜像,但不会自动跑,得手动
adb shell helloserver &。这叫"游离模块"。真正集成进系统、开机自动跑的做法见 L2(那才是 IMS/AMS 的路子)。
9. 常见问题分析与定位(Binder 实战)
Binder 是 Android 万物之基,线上问题极多。下面是真实高频的几类,以及怎么查。
9.1 常见问题清单
getService("xxx")返回 null —— 拿不到服务- 调用抛
DeadObjectException/transact failed—— 对端进程挂了 TransactionTooLargeException—— 一次传太多数据- App 卡死 / ANR,trace 显示卡在
BinderProxy.transact—— 同步 binder 调用对端处理慢 - 权限校验失效 /
getCallingUid拿到的 uid 不对
9.2 每个问题的分析与定位
问题1:getService 返回 null
- 现象:
interface_cast后调用崩溃,或getService返回空。 - 可能原因(高→低):① 服务还没起(addService 没执行) ② 服务名拼错 ③ SELinux 没放行(见 L6) ④ 服务进程崩了
- 定位步骤:
adb shell service list | grep hello # 服务在不在黄页里?不在=没 addService 成功 adb shell dumpsys -l | grep hello # 同上,另一种列法 adb logcat | grep -iE "avc.*hello|SELinux" # 有没有 avc denied(SELinux 拦了) adb shell ps -A | grep helloserver # 服务进程还活着吗 - 根因→修复:不在 service list → 查 addService 返回值/服务进程是否起;avc denied → 补 sepolicy(L6/L11)。
问题2:DeadObjectException
- 现象:之前能调,某次调用突然抛
DeadObjectException。 - 原因:服务端进程死了(crash/被 lmkd 杀)。你手里的 BinderProxy 成了"野指针"。
- 定位:
adb logcat | grep -iE "died|tombstone|lmkd.*kill" # 服务进程怎么没的 adb shell ls /data/tombstones/ # native crash 现场 - 修复:客户端要注册死亡通知(
linkToDeath),对端死了能感知并重连;别缓存 BinderProxy 当永久有效。
问题3:TransactionTooLargeException
- 现象:传大 Bundle/大 List/大 bitmap 时抛异常。
- 原因:Binder 一次事务的内核缓冲区有上限(每进程约 1MB,且多个并发调用共享)。
- 定位:看抛异常的调用传了多大数据;
dumpsys meminfo看 binder 缓冲。 - 修复:大数据别走 binder 参数,改用共享内存(ashmem/Bitmap.parcel via ashmem)、文件描述符、分页传输。
问题4:卡在 transact 的 ANR
- 现象:ANR trace 主线程栈顶是
android.os.BinderProxy.transactNative。 - 原因:同步 binder 调用,对端服务处理太慢(或对端也在等锁),把你主线程卡住。
- 定位:
adb shell dumpsys activity # 看 ANR # 抓 trace,看主线程卡在哪个 transact,再看对端服务那个方法为啥慢 adb pull /data/anr/traces.txt - 修复:① 别在主线程做同步 binder 调用 ② 用
oneway(异步,不等返回) ③ 优化对端实现。
问题5:getCallingUid 拿到的 uid 不对
- 现象:权限校验时 uid 不是预期的调用方,而是自己(system)。
- 原因:在调用
getCallingUid之前,代码里调过clearCallingIdentity()(把身份临时清成自己了)却没及时取回,或不在 binder 调用线程上取。 - 定位:检查调用链里有没有
clearCallingIdentity;getCallingUid必须在 binder 线程、transact 上下文里取才准。 - 修复:
clearCallingIdentity后务必配对restoreCallingIdentity(token);权限判断在进入服务方法的第一时间取 uid。
9.3 定位决策树
Binder 调用出问题
├─ 拿不到服务(null) → service list 有没有 → 没有=addService/进程问题;有=SELinux(L6)
├─ DeadObjectException → logcat died/tombstone/lmkd → 对端进程死了 → linkToDeath 重连
├─ TooLarge → 传了多大 → >~1MB → 改共享内存/fd
├─ ANR 卡 transact → traces.txt 主线程 → 对端慢 → oneway/异步/优化对端
└─ uid 不对 → 查 clearCallingIdentity 配对 → 第一时间取 uid
10. 小结(一句话记住)
- Binder = 一次拷贝 + 内核盖 UID/PID 戳 —— 快,且权限校验有根。
- 三角色:Proxy(客户端打包)→ Binder 驱动(内核转发+盖戳)→ Stub(服务端解包分发)→ Service(你的实现)。
- ServiceManager 是黄页:
addService上架,getService取货。 - AIDL 把跨进程调用自动化:写一行接口,Parcel 打包/transact 全自动生成。
- 排查口诀:拿不到查 service list+SELinux;挂了 linkToDeath;太大走共享内存;卡了用 oneway。
下节预告
L2:getSystemService 全链路 + 写一个真集成进 system_server 的 Java 系统服务 —— 把这一课的 native 思维升级到 framework Java 层,让服务开机自动起、App 能 getSystemService 拿到。