3. Binder机制深度剖析:Binder原理、ServiceManager、AIDL、Binder驱动、IPC通信流程、Binder线程池
Binder,可以说是Android系统里最核心的机制之一。我当年刚接触Android时,觉得这玩意儿就是个黑盒——反正调用了就能跨进程通信,至于里面怎么跑的,完全没概念。后来在做一个系统级App的性能优化时,频繁的IPC调用导致UI卡顿,逼得我不得不把Binder的源码翻了个底朝天。
说白了,Binder就是Android用来实现跨进程通信(IPC)的一套方案。你想想看,每个App跑在自己的进程里,怎么互相调用服务?怎么让系统服务给你干活?靠的就是Binder。
Binder原理:为什么是它?
传统的Linux IPC方式,比如管道、消息队列、共享内存、Socket,各有各的毛病。共享内存快但同步复杂,Socket通用但性能差。Android选Binder,核心原因就两个:性能和安全性。
Binder基于C/S架构,数据拷贝只需要一次(传统IPC需要两次)。嗯,这里要注意,一次拷贝意味着数据从发送进程的用户空间,直接拷贝到接收进程的内核空间,然后映射到接收进程的用户空间。省掉了一次拷贝,性能自然就上去了。
核心要点:Binder通信中,数据只拷贝一次。这是它比传统Socket、管道快的关键原因。
另外,Binder自带UID/PID校验。每个Binder调用都能拿到调用方的身份信息。我在项目中遇到过一个问题:某个恶意App伪装成系统服务来窃取数据。后来我们就是靠Binder的UID校验,在服务端做了白名单过滤,才堵住了这个漏洞。
ServiceManager:Binder世界的“黄页”
ServiceManager,你可以把它理解成一个电话本。系统里那么多服务——ActivityManagerService、WindowManagerService、PackageManagerService——它们都注册在ServiceManager里。客户端想用哪个服务,就去ServiceManager查一下,拿到一个Binder引用,然后直接通信。
注册流程是这样的:
- 服务端向ServiceManager注册,说“我叫AMS,这是我的Binder实体”
- ServiceManager记录下名字和Binder引用的对应关系
- 客户端通过名字查询,拿到Binder引用
- 客户端拿着这个引用,直接和服务端通信
ServiceManager本身也是一个Binder服务,它的句柄是0。所有Binder通信的第一步,都得先找到它。
个人经验:我曾经调试过一个诡异的问题——系统服务注册失败,导致App启动就崩溃。后来发现是ServiceManager的binder线程池满了,处理不过来注册请求。解决办法是增大ServiceManager的线程池上限。
AIDL:Binder的“翻译官”
AIDL(Android Interface Definition Language),说白了就是帮你自动生成Binder通信代码的工具。你写一个.aidl文件,定义好接口方法,编译时自动生成Java代码。省去了手写Binder模板代码的麻烦。
举个例子:
// IMyService.aidl
interface IMyService {
int add(int a, int b);
String getData(String key);
}
编译后,会生成IMyService.java,里面包含了Stub(服务端)和Proxy(客户端)两个内部类。Stub负责接收Binder驱动传来的数据,反序列化后调用你的实现方法。Proxy负责把参数序列化,通过Binder驱动发送给服务端。
我个人习惯是:所有跨进程接口都用AIDL定义。哪怕只是两个进程间传个简单的int值,也别手写Binder代码。为什么?因为AIDL生成的代码经过了充分测试,边界情况都处理好了。你自己手写,很容易漏掉线程安全、序列化异常这些坑。
避坑指南:我曾经手写过一次Binder通信代码,结果在传输大对象时出现了TransactionTooLargeException。AIDL生成的代码会自动做分片传输,手写的话你得自己处理。从那以后,我再也不手写Binder了。
Binder驱动:真正的“搬运工”
Binder驱动是Linux内核里的一个模块,路径在kernel/drivers/android/binder.c。它负责三件事:
- 数据传递:把数据从一个进程拷贝到另一个进程
- 线程管理:管理Binder线程池,分配线程处理请求
- 引用计数:管理Binder实体的引用,防止内存泄漏
Binder驱动里有个关键数据结构——binder_transaction。每次IPC调用,驱动都会创建一个transaction对象,记录下调用方、目标方、数据内容。然后通过binder_thread_read和binder_thread_write这两个函数,完成数据的收发。
你想想看,每次你调用一个远程方法,背后都是Binder驱动在默默干活。它把参数打包成Parcel,通过ioctl系统调用传给驱动,驱动找到目标进程的线程,把数据塞过去,再唤醒那个线程去处理。
IPC通信流程:一次完整的调用
一次完整的Binder IPC调用,大概分这么几步:
- 客户端调用Proxy方法,比如
add(1, 2) - Proxy把参数序列化成Parcel,调用
transact() - transact()通过ioctl进入Binder驱动
- 驱动找到服务端的Binder实体,把数据拷贝到服务端进程
- 服务端的线程池中某个线程被唤醒,调用Stub的
onTransact() - Stub反序列化参数,调用你的实现方法
- 返回值同样走一遍反向流程,回到客户端
整个过程,数据在用户空间和内核空间之间穿梭。但注意,只有一次数据拷贝——从客户端用户空间到内核空间,然后直接映射到服务端用户空间。服务端不需要再拷贝一次。
性能数据:一次Binder调用,从用户态到内核态再到用户态,大约耗时几十微秒。如果传输的数据量大,时间会线性增长。所以,不要在Binder调用中传大对象,比如几MB的Bitmap。否则你会看到TransactionTooLargeException。
Binder线程池:谁在干活?
Binder线程池,是每个进程里专门用来处理Binder请求的线程集合。默认情况下,一个进程的Binder线程池有16个线程(Android 8.0之后可以配置)。
线程池的工作机制是这样的:
- 当Binder驱动收到请求,会从目标进程的线程池里挑一个空闲线程
- 如果没有空闲线程,驱动会创建一个新的Binder线程(上限是16个)
- 如果16个线程都忙,请求就会排队等待
我在项目中遇到过一个问题:某个系统服务处理Binder请求特别慢,导致其他App调用这个服务时都卡住了。后来发现是服务端的Binder线程池被耗光了——16个线程全在等一个锁。解决办法是把耗时操作放到独立的工作线程里,Binder线程只负责分发请求。
调优建议:如果你的App需要频繁进行IPC调用,可以考虑增大Binder线程池。通过Process.setThreadPoolSize()可以设置。但别设太大,线程切换也是有开销的。我个人经验,32个线程基本够用了。
嗯,Binder机制就讲到这里。内容不少,但核心就三件事:一次拷贝、ServiceManager做路由、Binder驱动做搬运。理解了这些,你再看AIDL生成的代码,就不会觉得神秘了。