1. 为什么Binder值得你花时间啃透
做Android系统开发或者Framework层改造的同学,迟早会撞上Binder这堵墙。应用层写多了觉得进程间通信不就是发个消息嘛,但一旦你要改系统服务、加自定义Native Service、排查跨进程调用超时或者死锁,Binder的细节就会像潮水一样涌上来。我见过太多人在这块栽跟头:有人搞不清一次拷贝和两次拷贝的区别,面试被问得哑口无言;有人写的Native Service跑着跑着就卡死,查了三天才发现是线程池打满了;还有人改传输数据大小限制时改错了地方,导致系统起不来。
这篇文章就是把我这些年跟Binder打交道攒下来的东西系统性地倒出来。从架构设计到源码关键路径,从AIDL生成的代码到底干了什么,到实际调试中怎么用工具抓Binder调用,我都会掰开揉碎讲。不管你是刚接触Android系统开发的新人,还是已经能写系统服务但想搞清楚底层机制的老手,应该都能从里面找到对自己有用的东西。文章会涉及不少源码层面的分析,但我会尽量用生活化的类比先把道理讲通,再落到代码上,避免一上来就堆砌结构体定义。
需要提前说明的是,Binder的源码在不同Android版本间有差异,我主要以较新版本(Android 12-14这个区间)的代码为基准来梳理,核心机制从Android 8之后基本稳定,差异主要在细节实现和部分数据结构上。如果你手头的代码版本不同,思路是一致的,对照着看就行。
2. Binder的整体架构与设计哲学
2.1 从一次跨进程调用说起
假设你在应用里调用了getSystemService(Context.ACTIVITY_SERVICE)拿到ActivityManager,然后调用了getRunningAppProcesses()。这一行代码背后发生了什么?你的进程(客户端)需要跟system_server进程(服务端)通信,让后者去查当前运行的进程列表,再把结果返回给你。两个进程各有各的虚拟地址空间,默认情况下互相看不见对方的内存,怎么把请求传过去、把结果拿回来?
最朴素的做法是socket或者管道,但Android没走这条路。Android选了一个叫Binder的机制,它在内核里有一个驱动(/dev/binder),所有跨进程通信都经过这个驱动中转。客户端把数据写进驱动,驱动把数据交给服务端,服务端处理完再把结果写回驱动,驱动再还给客户端。听起来跟socket差不多?差别在于Binder在驱动层做了大量优化,尤其是内存映射那一块,让数据拷贝次数降到了最低。
我习惯用一个类比来理解Binder的角色:它就像一栋写字楼的前台。你想找楼里某家公司的某个人办事,你不能直接闯进去,得先到前台登记,前台确认你要找的人存在、你有权限,然后帮你把材料转交过去,对方办完事把结果交回前台,前台再给你。前台(Binder驱动)掌握着整栋楼(所有进程)的人员信息(Binder实体和引用),是唯一的权威中转站。
2.2 Binder的四层架构
从下往上,Binder可以分成四层:
内核层:Binder驱动,代码在drivers/android/binder.c(新版本拆成了多个文件,比如binder.c、binder_alloc.c等)。这一层负责最核心的数据传输、内存管理、线程管理、引用计数。它注册为一个字符设备,上层通过ioctl来跟它交互。
Native层:libbinder,C++实现,代码在frameworks/native/libs/binder/。这一层把ioctl的裸调用封装成了IPCThreadState、ProcessState、BpBinder、BBinder这些类,提供了面向对象的接口。ServiceManager也在这层。
Java层:frameworks/base/core/java/android/os/下的Binder.java、BinderProxy.java、Parcel.java等。Java层的Binder对象通过JNI跟Native层对应,android_util_Binder.cpp是桥梁。
应用层:AIDL生成的Stub和Proxy,以及各种Manager类(如ActivityManager)。开发者日常接触最多的就是这一层。
这四层里,真正干活的是内核层和Native层,Java层和应用层更多是封装和便利。理解Binder的关键在于搞清楚Native层怎么跟驱动交互,以及一次调用在驱动里走了哪些路径。
2.3 为什么不用socket、共享内存或者管道
这是个经典问题,面试也常问。简单对比一下:
| 机制 | 拷贝次数 | 安全性 | 易用性 | 适用场景 |
|---|---|---|---|---|
| socket/管道 | 2次以上 | 依赖上层协议 | 一般 | 通用IPC,跨主机 |
| 共享内存 | 0次(但需同步) | 差,需自行保证 | 复杂 | 大数据量传输 |
| 信号量/消息队列 | 2次 | 一般 | 一般 | 简单同步 |
| Binder | 1次 | 内核保证UID/PID | 好 | Android进程间调用 |
Binder的核心优势有三个:一是性能,通过mmap把接收方的内存映射到内核空间,发送方数据只需从用户空间拷贝到内核空间一次,接收方直接读映射区,省掉一次拷贝;二是安全性,内核在传输时会带上发送方的UID和PID,接收方可以据此做权限校验,socket做不到这一点(发送方可以伪造);三是面向对象,Binder把跨进程调用伪装成了本地对象调用,AIDL生成的代码让开发者几乎感觉不到进程边界。
共享内存虽然拷贝次数更少,但它没有内置的同步机制和权限控制,用起来要自己处理一堆并发问题,不适合Android这种需要频繁、小量、高安全要求的调用场景。
2.4 Binder通信模型中的四个角色
理解Binder必须搞清楚四个角色:
Client:发起调用的进程。它持有服务端的一个Binder引用(BpBinder),调用transact()发起请求。
Server:提供服务的进程。它有一个BBinder实体,收到请求后执行onTransact(),返回结果。
ServiceManager:服务注册与查询的中心。所有系统服务启动时都要向ServiceManager注册,客户端通过名字向ServiceManager查询,拿到Binder引用后才能调用。ServiceManager本身也是一个Binder服务,它的handle固定为0。
Binder驱动:内核中的中转站,管理Binder实体和引用的映射关系,负责数据搬运和线程调度。
这里有个容易混淆的点:Binder实体(binder_node)和Binder引用(binder_ref)是驱动里的概念。一个Server进程里的BBinder对象在驱动里对应一个binder_node,Client进程拿到的BpBinder在驱动里对应一个binder_ref,binder_ref指向binder_node。当Client调用时,驱动根据binder_ref找到binder_node,从而找到Server进程。
3. 一次Binder调用的完整生命周期
3.1 从Java层调用到Native层
还是用getRunningAppProcesses()这个例子。在应用进程里,ActivityManager实际上是一个IActivityManager的Proxy对象(ActivityManagerProxy),它实现了IActivityManager接口。当你调用getRunningAppProcesses()时,Proxy会做这几件事:
- 创建一个
Parcel对象(_data),把方法参数写进去。对于getRunningAppProcesses没有参数,但会写入一个整数标识这是哪个方法(INTERFACE_TRANSACTION或者AIDL生成的TRANSACTION_xxx)。 - 创建另一个
Parcel对象(_reply)用来接收返回值。 - 调用
mRemote.transact(GET_RUNNING_APP_PROCESSES_TRANSACTION, _data, _reply, 0)。这里的mRemote是一个IBinder,实际类型是BinderProxy。 BinderProxy.transact()是一个native方法,通过JNI调用到android_util_Binder.cpp里的android_os_BinderProxy_transact()。- 这个JNI函数拿到
BinderProxy对应的Native对象BpBinder,调用BpBinder::transact()。
到这里,调用从Java层进入了Native层。BpBinder::transact()会调用IPCThreadState::self()->transact(),IPCThreadState是每个线程一个实例的,它负责跟驱动打交道。
3.2 IPCThreadState与驱动的交互
IPCThreadState::transact()的核心逻辑是:
status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { // 把data写入mOut status_t err = data.errorCheck(); if (err == NO_ERROR) { err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr); } // 如果不需要等待回复(oneway),直接返回 if ((flags & TF_ONE_WAY) == 0) { // 等待回复 err = waitForResponse(reply); } else { // 发送后不等待 err = talkWithDriver(); } return err; }writeTransactionData()把数据打包成binder_transaction_data结构,写入mOut这个Parcel。然后waitForResponse()会调用talkWithDriver(),后者通过ioctl跟驱动通信:
status_t IPCThreadState::talkWithDriver(bool doReceive) { binder_write_read bwr; bwr.write_size = mOut.dataSize(); bwr.write_buffer = (uintptr_t)mOut.data(); bwr.read_size = mIn.dataCapacity(); bwr.read_buffer = (uintptr_t)mIn.data(); ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr); // 处理返回的数据 }BINDER_WRITE_READ是跟驱动交互的核心命令,它同时携带了要写的数据和用来读的缓冲区。驱动处理完写请求后,如果有回复,会填充读缓冲区。
3.3 驱动层的处理路径
驱动收到BINDER_WRITE_READ后,会解析binder_write_read结构,先处理写部分。写部分是一串命令,每条命令以uint32_t开头标识类型,比如BC_TRANSACTION、BC_REPLY、BC_FREE_BUFFER等。
对于BC_TRANSACTION,驱动会:
- 根据
handle找到目标binder_ref,再找到binder_node,从而确定目标进程。 - 把数据从发送方的用户空间拷贝到内核空间(这是那"一次拷贝")。
- 在目标进程的
binder_proc里分配一块内存(通过binder_alloc),把数据映射过去。实际上,驱动会尝试从目标进程的mmap区域里分配空间,然后把数据拷贝到那块区域。由于目标进程已经mmap了/dev/binder,这块区域在目标进程的用户空间是可见的,所以不需要再拷贝一次。 - 把目标进程的某个等待线程唤醒(或者如果目标进程没有空闲线程,就排队等待)。
目标进程的IPCThreadState在talkWithDriver()里阻塞在ioctl上,被唤醒后读到BR_TRANSACTION命令,然后调用executeCommand()处理:
case BR_TRANSACTION: { binder_transaction_data tr; // 从mIn读取tr if (tr.target.ptr) { // 有本地BBinder sp<BBinder> b((BBinder*)tr.cookie); error = b->transact(tr.code, buffer, &reply, tr.flags); } else { // 没有本地对象,交给context object error = the_context_object->transact(tr.code, buffer, &reply, tr.flags); } // 如果需要回复,发送BC_REPLY }BBinder::transact()会调用onTransact(),这是服务端真正执行业务逻辑的地方。执行完后,如果有返回值,会通过BC_REPLY命令把结果写回驱动,驱动再转交给客户端。
3.4 数据拷贝的真相
很多人说Binder是"一次拷贝",这个说法需要精确理解。准确地说,是发送方的数据从用户空间拷贝到内核空间一次,接收方通过mmap直接访问内核空间里的数据,不需要再拷贝。但接收方在处理数据时,如果要把数据从Parcel里读出来(比如读一个String),那还是会有一次拷贝,只不过这次拷贝是在接收方进程内部,不涉及跨进程。
对比socket:发送方用户空间→内核空间→接收方用户空间,两次拷贝。Binder省掉了第二次。共享内存是零次拷贝,但需要自己处理同步和权限。
这个优化在频繁小数据量传输时效果明显。Android里大量跨进程调用都是小数据量(比如传递一个int、一个String),Binder的优势就体现出来了。
4. AIDL背后的代码生成机制
4.1 AIDL文件到Java代码的映射
写一个AIDL文件,比如:
// IMyService.aidl package com.example.demo; interface IMyService { int add(int a, int b); String getInfo(); void doSomething(); }编译时会生成IMyService.java,里面有一个Stub类和一个Proxy类。Stub是服务端要继承的,Proxy是客户端拿到的。
生成的代码里,方法调用被映射成了transact()调用。add方法对应的transaction code是TRANSACTION_add(一个常量,通常是IBinder.FIRST_CALL_TRANSACTION + 0)。Proxy.add()的实现大致是:
@Override public int add(int a, int b) throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); int _result; try { _data.writeInterfaceToken(DESCRIPTOR); _data.writeInt(a); _data.writeInt(b); mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0); _reply.readException(); _result = _reply.readInt(); } finally { _reply.recycle(); _data.recycle(); } return _result; }Stub.onTransact()的实现:
@Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { switch (code) { case TRANSACTION_add: { data.enforceInterface(DESCRIPTOR); int _arg0 = data.readInt(); int _arg1 = data.readInt(); int _result = this.add(_arg0, _arg1); reply.writeNoException(); reply.writeInt(_result); return true; } case TRANSACTION_getInfo: { data.enforceInterface(DESCRIPTOR); String _result = this.getInfo(); reply.writeNoException(); reply.writeString(_result); return true; } // ... } return super.onTransact(code, data, reply, flags); }这里有几个关键点值得注意:
writeInterfaceToken和enforceInterface:这是用来校验接口的。客户端写入DESCRIPTOR(通常是包名+接口名),服务端检查是否匹配。如果不匹配就抛异常。这防止了客户端调用错误的接口。
writeNoException和readException:服务端执行方法时如果抛了异常,会通过writeException把异常信息写进reply,客户端readException时重新抛出。这样跨进程调用也能传递异常。
Parcel的读写顺序必须严格一致:客户端按什么顺序写,服务端就必须按什么顺序读。写一个int读一个int,写一个String读一个String。顺序错了就会读到错误的数据,甚至崩溃。这是新手最容易犯的错误之一。
4.2 oneway关键字的影响
如果AIDL方法声明为oneway,生成的代码会不一样。oneway表示调用是异步的,客户端发起调用后立即返回,不等待服务端执行结果。生成的Proxy代码里,transact()的flags会带上IBinder.FLAG_ONEWAY:
mRemote.transact(Stub.TRANSACTION_doSomething, _data, null, IBinder.FLAG_ONEWAY);注意_reply传的是null,因为不需要接收返回值。服务端onTransact()里,oneway方法执行完后不会发送BC_REPLY。
oneway的使用有讲究。它适合那些不需要返回值、不需要确认执行结果的操作,比如日志上报、状态通知。但要注意,oneway调用是排队的,如果服务端处理慢,请求会堆积。而且oneway调用不保证顺序(实际上在同一个Binder对象上,oneway调用是保序的,但跨Binder对象就不保证了)。
还有一个坑:oneway方法不能有返回值,也不能抛异常(抛了也没人接收)。AIDL编译器会检查这一点。
4.3 Parcel的序列化细节
Parcel是Binder传输的数据容器,它的设计很讲究。Parcel内部是一块连续的内存,写入数据时按顺序追加,读取时按顺序消费。它支持基本类型(int、long、float、double、boolean)、String、数组、List、Map、Parcelable、IBinder等。
几个容易踩坑的地方:
String的写入:writeString()会先写一个int表示长度(-1表示null),然后写UTF-16的字符数据。读取时readString()先读长度,再读字符。如果写入的是null,读出来也是null。
Parcelable的写入:需要实现writeToParcel()和CREATOR。写入时会先写类名(用于查找CREATOR),再写数据。读取时根据类名反射找到CREATOR,调用createFromParcel()。这个过程有反射开销,频繁传输大量Parcelable时要注意性能。
IBinder的写入:writeStrongBinder()会把IBinder对象写入Parcel。如果是一个本地BBinder,驱动会为它在目标进程创建一个binder_ref;如果是一个BpBinder(远程引用),驱动会做引用传递。这是Binder支持对象传递的关键。
Parcel的大小限制:默认情况下,Parcel的传输缓冲区是1MB(BINDER_VM_SIZE,在ProcessState.cpp里定义)。但这个1MB是整个进程共享的,不是每次调用1MB。如果并发调用多,或者单次传输数据大,很容易触发TransactionTooLargeException。这个限制在驱动层也有对应的binder_alloc缓冲区大小限制。
注意:1MB的限制是进程级别的,不是单次调用的。如果你在一个进程里同时有多个Binder调用在传输数据,它们共享这1MB。所以大文件传输绝对不要走Binder,应该用文件描述符传递或者ashmem共享内存。
5. Binder线程管理与并发模型
5.1 Binder线程池的工作机制
每个进程在启动Binder通信时,会通过ProcessState::self()->startThreadPool()启动一个线程池。默认情况下,主线程会加入Binder线程池,另外还会启动若干个子线程。线程数量由binder_proc的max_threads控制,默认是15(DEFAULT_MAX_BINDER_THREADS)。
当驱动收到一个BC_TRANSACTION请求,需要目标进程处理时,它会看目标进程有没有空闲的Binder线程。如果有,唤醒一个;如果没有,且线程数没达到上限,会通知目标进程创建新线程;如果达到上限,请求会排队等待。
这个机制导致一个常见问题:如果服务端的所有Binder线程都在处理耗时操作,新的请求就会排队,客户端会阻塞。更严重的是,如果服务端的某个Binder线程又去调用另一个服务(形成嵌套调用),而那个服务又在等这个服务,就可能死锁。
我遇到过的一个真实场景:某系统服务在处理请求时,同步调用了另一个系统服务,而那个服务恰好也在等这个服务的另一个请求。两边线程池都满了,互相等待,整个系统卡死。排查这种问题非常痛苦,因为日志里只看到一堆binder transaction超时。
5.2 线程池大小的调整
DEFAULT_MAX_BINDER_THREADS是15,这个值在ProcessState.cpp里:
#define DEFAULT_MAX_BINDER_THREADS 15可以通过ProcessState::setThreadPoolMaxThreadCount()调整,但一般不建议改。15个线程对大多数服务够用了,改大了反而增加内存开销和上下文切换成本。
真正需要关注的是避免在Binder线程里做耗时操作。如果某个操作确实耗时,应该把它丢到工作线程去,Binder线程立即返回。或者用oneway调用,让客户端不阻塞。
5.3 嵌套调用的风险与应对
嵌套调用是指服务端在处理Binder请求时,又发起了另一个Binder调用。这在系统服务里很常见,比如ActivityManagerService调用PackageManagerService。
嵌套调用的风险在于线程占用。假设服务A的线程池有15个线程,每个线程都在处理请求,每个请求又去调用服务B。服务B的线程池也有15个线程,也在处理请求,每个请求又回调服务A。如果两边同时打满,就会死锁。
应对方法有几个:
一是减少嵌套。能异步的就异步,能合并的就合并。比如把多个小调用合并成一个大调用。
二是设置超时。Binder调用可以设置超时,超时后返回错误,避免无限等待。但超时设置要合理,太短会误判,太长失去意义。
三是用oneway打破循环。如果某个调用不需要返回值,用oneway可以避免线程占用。
四是监控线程池状态。通过dumpsys binder或者/sys/kernel/debug/binder/下的文件可以查看Binder线程状态,及时发现异常。
6. ServiceManager与服务注册发现
6.1 ServiceManager的角色
ServiceManager是Android Binder机制的"电话簿"。所有系统服务在启动时都要向它注册,客户端通过名字查询服务。ServiceManager本身也是一个Binder服务,它的Binder句柄固定为0(BINDER_SERVICE_MANAGER)。
ServiceManager的启动很早,在init进程解析init.rc时就会启动。它的实现分两部分:servicemanager进程(Native层)和IServiceManager接口(Java层通过ServiceManager类访问)。
注册服务的流程:
// 服务端 defaultServiceManager()->addService(String16("my.service"), new MyService());查询服务的流程:
// 客户端 sp<IBinder> binder = defaultServiceManager()->getService(String16("my.service")); sp<IMyService> service = IMyService::asInterface(binder);asInterface()会判断传入的IBinder是本地对象还是远程代理。如果是本地(BBinder),直接返回Stub;如果是远程(BpBinder),创建Proxy。
6.2 服务注册的权限控制
ServiceManager在注册服务时会检查权限。默认情况下,只有system用户(UID 1000)和root用户(UID 0)可以注册服务。这个检查在servicemanager的svc_can_register()函数里:
static bool svc_can_register(const uint16_t *name, size_t name_len, pid_t spid, uid_t uid) { // 检查uid是否是root或system if (uid == AID_ROOT || uid == AID_SYSTEM) return true; // 检查是否有对应的selinux权限 // ... return false; }从Android 8开始,SELinux策略进一步收紧了服务注册权限。普通应用进程无法注册系统服务,必须是有对应SELinux域的系统进程才行。这防止了恶意应用冒充系统服务。
6.3 服务查询的缓存机制
客户端每次getService()都会向ServiceManager发起Binder调用,这有开销。所以很多客户端会缓存查询结果。比如ActivityManager在第一次获取后会把IActivityManager对象缓存起来,后续直接使用。
但缓存有个问题:如果服务重启了,缓存的Binder引用就失效了。这时候调用会抛DeadObjectException。所以健壮的客户端需要处理DeathRecipient,在服务死亡时收到通知,清理缓存并重新查询。
IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { @Override public void binderDied() { // 服务死了,清理缓存 mService = null; // 可以选择重新绑定 } }; binder.linkToDeath(recipient, 0);linkToDeath是Binder提供的重要机制,它让客户端能感知服务端的死亡。系统服务里大量使用了这个机制,比如ActivityManagerService会监听各种服务的死亡,及时做清理。
7. 实战调试与问题排查
7.1 常用调试工具
dumpsys:最常用的工具。dumpsys -l列出所有服务,dumpsys activity查看ActivityManager状态,dumpsys binder查看Binder状态。dumpsys binder会输出每个进程的Binder线程数、事务数、引用数等信息。
/sys/kernel/debug/binder/:内核暴露的Binder调试信息。需要root权限。里面有stats、transactions、transaction_log等文件。transaction_log记录了最近的Binder事务,排查死锁时非常有用。
binderfs:新版本内核用binderfs替代了debugfs,路径在/dev/binderfs/或者/sys/fs/binder/。功能类似。
ftrace:可以跟踪Binder驱动的函数调用,比如binder_transaction、binder_thread_read等。适合分析性能问题。
Perfetto:Android的官方性能分析工具,可以抓取Binder调用的时间线,直观看到每次调用的耗时和调用链。
7.2 典型问题排查
问题一:TransactionTooLargeException
这个异常表示传输数据超过了1MB限制。排查方法是看异常堆栈,找到是哪个调用传输了大量数据。常见原因是传输了大的Bitmap、长String、大List。
解决方法:把大数据拆分成多次小传输,或者用文件描述符传递(ParcelFileDescriptor),或者用ashmem共享内存。
问题二:Binder调用超时
Binder调用默认没有超时(同步调用会一直等)。如果服务端卡住,客户端会一直阻塞。排查方法是看服务端的Binder线程状态,是不是所有线程都在忙。
可以用dumpsys binder查看,或者看/sys/kernel/debug/binder/proc/<pid>下的线程信息。
问题三:DeadObjectException
服务端进程死了,客户端调用时抛这个异常。排查方法是看服务端为什么死(可能是崩溃、被kill、OOM)。客户端应该注册DeathRecipient,在服务死亡时做清理和重连。
问题四:Binder线程池耗尽
表现为新的Binder调用一直排队,客户端超时。排查方法是看服务端的Binder线程数是否达到上限,以及线程都在干什么。常见原因是某个Binder方法执行时间太长,或者发生了嵌套调用死锁。
7.3 一个真实的排查案例
之前遇到一个系统服务偶发卡死的问题。现象是某个功能偶尔无响应,日志里看到大量binder transaction failed。用dumpsys binder查看,发现该服务的Binder线程数一直是15(上限),所有线程都在waitForResponse状态。
进一步用ftrace跟踪,发现这些线程都在等另一个服务的回复。而那个服务的线程池也满了,线程都在等第三个服务。形成了一个调用环。
根因是三个服务之间的调用关系设计有问题,在高并发时形成了循环等待。解决方法是把其中一个调用改成异步(oneway),打破循环。同时给关键调用加了超时,避免无限等待。
这个案例的教训是:设计系统服务时一定要画清楚调用关系图,避免循环依赖。如果无法避免,至少要把其中一条边改成异步。
8. 性能优化与进阶话题
8.1 减少Binder调用次数
Binder调用有固定开销(ioctl、线程切换、数据拷贝),频繁的小调用累积起来很可观。优化方法是合并调用。比如一个接口需要获取多个字段,与其定义多个getter方法,不如定义一个方法一次性返回所有字段。
AIDL里可以用Parcelable打包多个字段,一次传输。或者用Bundle。但要注意Bundle本身也有序列化开销,字段太多时不一定划算。
8.2 大数据传输的正确姿势
前面说过,Binder传输限制是1MB。超过这个大小的数据,正确做法是用ParcelFileDescriptor传递文件描述符,或者用ashmem(Android的匿名共享内存)。
ParcelFileDescriptor的用法是:服务端创建一个管道或者打开一个文件,把文件描述符通过Binder传给客户端,客户端通过这个描述符读写数据。数据本身不经过Binder,绕开了大小限制。
ashmem的用法是:服务端创建一块共享内存区域,把文件描述符传给客户端,客户端mmap这块区域直接读写。适合大块数据的零拷贝传输。
8.3 Binder与hidl、aidl的演进
Android 8引入了HIDL(HAL Interface Definition Language),用于HAL层与Framework层的通信。HIDL底层也是Binder,但用了不同的驱动(/dev/hwbinder)和不同的线程模型。Android 11之后,AIDL开始支持HAL,HIDL逐渐被替代。
AIDL在Android 10之后增加了稳定版本(stable AIDL)的支持,允许跨版本传输。这对Treble架构下的Vendor与System分区通信很重要。
这些演进的核心思路是一致的:用接口定义语言描述跨进程接口,自动生成序列化和反序列化代码,底层用Binder传输。理解了这个核心,具体用哪个工具就是查文档的事了。
8.4 Binder的未来
Binder机制从Android诞生至今,核心设计没有大改。它的性能、安全性、易用性平衡得很好,短期内不会被替代。但也有一些新的探索,比如基于共享内存的IPC、基于io_uring的异步IPC等。这些在特定场景下可能有优势,但通用性和生态成熟度还远不如Binder。
对于开发者来说,把Binder吃透,比追逐新机制更实际。因为Android系统里到处都是Binder,改任何一个系统服务都绕不开它。
我个人在实际排查Binder问题时,最深的体会是:日志和工具比直觉可靠。很多次我以为问题在A处,结果用ftrace一跟,发现是B处。Binder的调用链可能很长,跨多个进程,靠猜是猜不准的。养成用工具的习惯,能省下大量时间。另外,设计阶段就把调用关系理清楚,比事后排查划算得多。我见过太多项目因为服务间调用关系混乱,后期维护成本极高。这一点,值得每个做系统开发的人重视。