Binder原理:进程间通信基础
讲Binder优化之前,咱得先把地基打牢。很多做Android开发的朋友,用Binder用得挺溜,但一问到「用户态和内核态到底怎么切换的」、「序列化到底干了什么事」,就有点含糊了。这不行,真遇到性能问题,你连瓶颈在哪都找不到。
我记得刚接触Binder那会儿,也觉得它就是「调用远程方法」而已。直到有一次线上问题:系统服务响应超时,我顺着调用栈一路追下去…嗯,才发现自己对Binder的理解只浮在API层面。今天咱们就把它铺开,把用户态、内核态、共享内存、序列化这几个核心概念讲透。
用户态与内核态:谁在干活?
Android的进程跑在用户态。用户态说白了就是「你写的代码」在跑,权限有限。比如你不能直接读写物理内存、不能直接操作硬件。内核态就不一样了,它拥有最高权限,能访问所有硬件资源。
Binder通信,本质上是两个用户态进程(比如App和SystemServer)要交换数据。但两个进程的内存空间是隔离的,不能互相访问。怎么办?必须经过内核。Binder驱动跑在内核态,充当「中间人」。
关键理解:
- 发起方(客户端)在用户态调用Binder API
- 数据先复制到内核空间(由Binder驱动完成)
- 内核再通知目标进程(服务端),把数据从内核空间复制到它的用户空间
- 一次Binder IPC,至少经历 一次用户态→内核态切换 + 一次内核态→用户态切换
为什么会多一次拷贝?别急,下面会讲共享内存来优化。
共享内存:Binder的「偷懒」之道
传统IPC(比如管道、Socket)通常需要两次数据拷贝:先从发送方用户态拷到内核态,再从内核态拷到接收方用户态。Binder默认也是两次拷贝——但它在某些场景下可以做到 一次拷贝。
怎么做到的?通过内存映射(mmap)。Binder驱动在内核空间维护一块缓冲区,同时把这缓冲区映射到用户空间。说白了,就是一块物理内存,内核和用户进程都能看到。
我个人的理解:
想象一下,你(客户端)想把一箱货交给对面楼的人(服务端)。传统做法是:你把货搬到自己楼下(用户态→内核态),再搬到他楼下(内核态→用户态)。Binder的做法是:你们俩共享一个地下仓库(mmap),你直接把货放在仓库里,他直接去拿,中间只需要一次「开门通知」。
不过要注意,不是所有Binder调用都能享受一次拷贝。只有数据量小于Binder缓冲区大小(通常1MB可以调)且使用Binder内核驱动内部的mmap机制才能实现。我自己在优化系统服务时,就遇到过因为传输大图片导致频繁两次拷贝的问题,后来改成用共享内存加文件描述符传递,才压下去延迟。
序列化与反序列化:数据怎么「打包」?
进程间通信,数据不能直接传对象,因为对象引用在另一个进程里没意义。你得把对象「拍平」成二进制字节流,这叫序列化。另一端收到二进制流后,再恢复成对象,这叫反序列化。
Android里常见的序列化方案有:
| 方案 | 特点 | 我踩过的坑 |
|---|---|---|
| Parcelable | Android原生,效率高,需手写实现 | 字段顺序不能乱改,不然反序列化会崩 |
| Serializable | Java原生,简单但慢,反射开销大 | 在Binder里用Serializable?性能直接腰斩 |
| Protocol Buffers | 跨语言,压缩率高 | 需要维护.proto文件,调试不方便 |
Binder内部强制要求使用Parcelable。你写的AIDL接口,编译器自动生成Parcelable的序列化代码。但要注意:AIDL默认传输的是Parcelable的浅拷贝。如果对象内部有大量数组或嵌套对象,每次序列化都会遍历所有字段,这个开销不可忽视。
我曾经优化过一个系统服务,每次调用要传输一个包含1000个元素的List。反序列化时,Binder驱动逐个解包,光这个步骤就占了整个IPC时间的40%。后来我把List改成文件描述符+共享内存,才把响应时间从50ms压到5ms。
Binder IPC完整流程一览
咱们把上面讲的内容串起来,看一张图就清楚多了:
我曾经踩过的坑:
- 在AIDL接口里传了一个很大的Bitmap对象,序列化直接把binder线程耗尽,系统服务响应超200ms
- 用了Parcelable但没注意成员变量里的List大小,反序列化耗时跟数据量呈线性增长
- 以为mmap能解决一切,忽略了Binder缓冲区大小限制(默认1MB),传大文件时直接TransactionTooLargeException
你想想看,Binder一顿操作下来,用户态到内核态两次切换是逃不掉的。但数据拷贝次数能优化。我们做性能优化时,首要目标就是减少拷贝、减少序列化开销。后面几章我会具体聊这些优化手段,今天先把基础打牢。
小结一下:
- 用户态/内核态:Binder通信必须经过内核驱动,这是安全的基石
- 共享内存mmap:让内核缓冲区映射到用户空间,有机会做到一次拷贝
- 序列化/反序列化:Parcelable是Binder的标配,注意控制数据量
- 性能关键点:数据拷贝次数、序列化字段数量、binder线程池负载
记住这些概念,后面分析具体案例时,你就能一眼看出问题在哪。我就是这样一步步从「调了接口但没效果」变成「看一眼堆栈就知道瓶颈」的。你也一定可以。