1 序
基于Android10分析更新
Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有管道,system V IPC(消息队列/共享内存/信号量),socket等IPC手段,却还要倚赖Binder来实现进程间通信,说明Binder具有无可比拟的优势。
为什么要学习理解Binder?
作为 Android 工程师,是不是常常会有这样的疑问:
- 为什么 Activity 间传递对象需要序列化?
- Activity 的启动流程是什么样的?
- 四大组件底层的通信机制是怎样的?
- AIDL 内部的实现原理是什么?等等…
这些问题的背后都与 Binder 有莫大的关系,要弄懂上面这些问题理解 Bidner 通信机制是必须的。
本文主要站在Android开发的角度来大致解析下Binder在java层的一些知识原理,不会深入源码细节。重点如下:
- 一些Linux的预备知识
- Binder到底是什么?
- Binder机制是如何跨进程的?
- 一次Binder通信的基本流程是什么样?
- 深入理解Java层的Binder
2 Linux 基础
由于Android系统基于Linux内核,我们有必要了解下Linux的一些基础 知识。
2.1 为什么需要跨进程通信(IPC)
上图展示了 Liunx 中跨进程通信涉及到的一些基本概念:
- 进程隔离
- 进程空间划分:用户空间(User Space)/内核空间(Kernel Space)
- 系统调用:用户态/内核态
进程隔离
进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术。这个技术是为了避免进程A写入进程B的情况发生。 进程的隔离实现,使用了虚拟地址空间。进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A将数据信息写入进程B。
虚拟内存地址空间
在32位系统里,物理内存寻址大小为:4G。

用32位表示一个地址块(上图的一个格子),最多能显示2^32个格子,也就是4 * 2^30 = 4G。
在编写程序的过程中,并不能直接访问物理内存地址。系统设计了虚拟地址(逻辑地址)来给每个进程分配地址空间。

同样的,虚拟地址寻址空间也是4G。只是被分为了两部分:内核地址空间和用户地址空间。其中内核地址空间占用1G,用户地址空间占用3G。
普通的应用程序只能访问3G的用户空间,内核、驱动等运行在内核地址空间,每个进程内核地址空间是共享的。应用程序想要操作网络、磁盘等硬件资源需要通过内核来访问。
虚拟地址如何映射到物理内存呢?

每个进程分配的虚拟地址空间都是独立的,通过页表映射到物理内存,进而读写数据。进程的虚拟地址空间既然是独立的,那么各个进程之间自然无法直接访问。
用户空间/内核空间
Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。对于Kernel这么一个高安全级别的东西,显然是不容许其它的应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是拒绝被访问的,于是就把Kernel和上层的应用程序抽像的隔离开,分别称之为Kernel Space和User Space,如上图所示。
用户态与内核态
虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。
Linux 使用两级保护机制:
- 0 级供系统内核使用
- 3 级供用户程序使用
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。
系统调用主要通过如下两个函数来实现:
1 | |
2.2 Linux下的传统 IPC 通信原理
1、共享物理内存。
2、通过内核中转。
2.2.1 共享物理内存
多个进程共享同一段物理内存,当某个进程改变内存内容时,其它进程都能够知道。此种方式无需拷贝内容,但是需要信号量进行进程间同步。

如图所示,进程A向进程B发送一段内容”hello world“,由于共享了内存,因此双方都可以直接从里面拿数据。享内存虽然无需拷贝,但控制复杂,难以使用。
2.2.2 通过内核中转
管道、消息队列、套接字(socket)使用的是这种方式

同样是进程A向进程B发送一段内容:先将A发送的内容拷贝到内核,这过程可以理解为存储,再从内核拷贝到B的用户空间,这过程可以理解为转发,因此一次”存储-转发”过程需要两次内容拷贝。
虽然Linux提供了上述(还有其它的如信号量等)的IPC方式,但是由于每种方式都有其缺点,因此Android弄了另一种方式:Binder。
2.2.3 mmap内存映射
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享,是一种不需要拷贝的方式。

而Binder IPC 机制中涉及到的内存映射就是通过 mmap() 来实现的。
3 Binder通信实现原理
正如前面所说,跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
一次完整的 Binder IPC 通信过程通常是这样:
- 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关 系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
如下图所示:

由上图可知,一次IPC通讯Binder只进行了一次数据拷贝。
对比下各种IPC方式数据拷贝次数:
| IPC | 数据拷贝次数 |
|---|---|
| 共享内存 | 0 |
| Binder | 1 |
| Socket/管道/消息队列 | 2 |
4 Binder 通信模型
Binder使用Client-Server通信方式,定义了四个角色:Server,Client,ServiceManager以及Binder驱动。其中Server,Client,ServiceManager运行于用户空间,驱动运行于内核空间,如下图所示:

整个通信步骤如下:
- 首先init进程预先启动了ServiceManager并成功注册成为Binder机制的上下文管理者,它需要在系统运行期间处理client端的请求。ServiceManager和其它进程同样采用Binder通信,ServiceManager是Server端,有自己的Binder对象,其它进程都是Client(相对于ServiceManager而言),需要通过这个Binder的引用来实现Binder的注册,查询和获取。
- 各个Server通过驱动向ServiceManager注册Binder(Server 中的 Binder 实体),ServiceManager内部维护了一张表,对应着各个Server的名字和地址。
- Server向ServiceManager注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用然后就能实现和 Server 的通信。
与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,遍布于client中的入口可以看成指向这个binder对象的‘指针’或者说该对象的引用。一旦获得了这个‘引用’就可以调用该对象的方法访问server。
上文已经解释过Client、Server 借助 Binder 驱动完成跨进程通信的实现机制,但是还有个问题会让我们困惑。A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object。
假设Client进程想要调用Server进程的object对象的一个方法add;对于这个跨进程通信过程,我们来看看Binder机制是如何做的:

如上图所示,前面我们介绍过跨进程通信的过程都有 Binder 驱动的参与,因为在数据流经 Binder 驱动的时候驱动会对数据做一层转换。当 Client 进程想要获取 Server 进程中的 object 时,驱动并不会真的把 object 返回给 Client,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 Server 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。对于 Client 进程来说和直接调用 object 中的方法是一样的。中间Binder驱动为我们做了一切。
当 Binder 驱动接收到 Client 进程调用add方法后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 Server 进程 object 的代理对象。于是就会去通知 Server 进程调用 object 的add方法,并要求 Server 进程把返回结果发给自己。当驱动拿到 Server 进程的返回结果后就会转发给 Client 进程,一次IPC通信就完成了。
一句话总结就是:
Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。
5 Binder的完整定义
现在我们可以对 Binder 做个更加全面的定义了:
- 从进程间通信的角度看,Binder 是一种进程间通信的机制;
- 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
- 从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
- 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象自动完成代理对象和本地对象之间的转换。
6 通过AIDL了解Binder跨进程调用
通常我们在做开发时,实现进程间通信用的最多的就是 AIDL。当我们定义好 AIDL 文件,在编译时编译器会帮我们生成代码实现 IPC 通信。借助 AIDL 编译以后的代码能帮助我们进一步理解 Binder IPC 的通信原理。
6.1 创建.aidl文件
我们创建一个IRemoteService.aidl文件,并在内声明一个方法getPid()用于返回服务进程id
1 | |
6.2 生成接口文件
构建应用程序时,SDK 工具会在项目gen/目录中生成接口文件。生成的文件名与.aidl文件名匹配,但带有.java扩展名(例如,IRemoteService.aidl结果为IRemoteService.java)。 下面就是根据IRemoteService.aidl生成为IRemoteService.java接口:
1 | |
系统帮我们生成了这个文件之后,我们只需要继承IRemoteService.Stub这个抽象类,实现它的方法,然后在Service 的onBind方法里面返回就实现了AIDL。
6.3 实现接口向客户端提供服务
1 | |
6.4 客户端链接服务进行IPC通信
1 | |
至此,一个使用aidl来进行IPC通信的示例就完成了。我们来一步一步分析源码,看看内部到底做了些什么。
6.5 AIDL过程分析
通过上面可以看到系统为我们生成了IRemoteService.java文件,它是一个接口,继承自IInterface接口,内部有我们之前定义的对外部提供的获取进程id的getPid()方法。
IRemoteService内部有一个Stub抽象类,继承自Binder类并实现IRemoteService,意味着这个Stub其实自己是一个Binder本地对象,并且对外提供了客户端所需要的服务(getPid())。
接着我们来看下Stub类的asInterface方法:
1 | |
首先看函数的参数IBinder类型的obj,这个对象是驱动给我们的,如果是Binder本地对象,那么它就是Binder类型,如果是Binder代理对象,那就是BinderProxy类型;然后,正如上面自动生成的文档所说,它会试着查找Binder本地对象,如果找到,说明Client和Server都在同一个进程,这个参数直接就是本地对象,直接强制类型转换然后返回,如果找不到,说明是远程对象(处于另外一个进程)那么就需要创建一个Binde代理对象,让这个Binder代理实现对于远程对象的访问。一般来说,如果是与一个远程Service对象进行通信,那么这里返回的一定是一个Binder代理对象,这个IBinder参数的实际上是BinderProxy;
再看看我们对于aidl的getPid方法的实现;在Stub类里面,getPid是一个抽象方法,我们需要继承这个类并实现它;如果Client和Server在同一个进程,那么直接就是调用这个方法;那么,如果是远程调用,这中间发生了什么呢?Client是如何调用到Server的方法的?
`
Stub类内部有一个内部类Proxy,也就是Binder代理对象。我们知道,对于远程方法的调用,是通过Binder代理完成的,在这个例子里面就是Proxy类,Proxy对于getPid方法的实现如下:
1 | |
它首先用Parcel把数据序列化了,然后调用了transact方法;这个transact到底做了什么呢?这个Proxy类在asInterface方法里面被创建,前面提到过,如果是Binder代理那么说明驱动返回的IBinder实际是BinderProxy, 因此我们的Proxy类里面的mRemote实际类型应该是BinderProxy;我们看看BinderProxy的transact方法:
1 | |
里面调用了transactNative方法,它的实现在native层frameworks/base/core/jni/android_util_Binder.cpp文件中的android_os_BinderProxy_transact方法。这个方法最终将通信过程交给了Binder驱动去完成。通过ioctl系统调用,Client进程陷入内核态,Client调用add方法的线程挂起等待返回;驱动完成一系列的操作之后唤醒Server进程,调用了Server进程本地对象的onTransact函数(实际上由Server端线程池完成)。我们再看Binder本地对象的onTransact方法(这里就是Stub类里面的此方法):
1 | |
在Server进程里面,onTransact根据调用号(每个AIDL函数都有一个编号,在跨进程的时候,不会传递函数,而是传递编号指明调用哪个函数)调用相关函数;在这个例子里面,调用了Binder本地对象的getPid方法;这个方法将结果返回给驱动,驱动唤醒挂起的Client进程里面的线程并将结果返回。于是一次跨进程调用就完成了。
我们回顾下使用AIDL接口时的相关类IBinder、IInterface、Binder、BinderProxy、IRemoteService、Stub、Proxy(IRemoteService、Stub、Proxy 由编译工具会给我们生成):
IBinder是一个接口,它代表了一种跨进程传输的能力;只要实现了这个接口,就能将这个对象进行跨进程传递;这是驱动底层支持的;在跨进程数据流经驱动的时候,驱动会识别IBinder类型的数据,从而自动完成不同进程Binder本地对象以及Binder代理对象的转换。IInterface内部只有一个方法asBinder,它返回的是一个IBinder。作用是检索与此接口关联的Binder对象,本地(同进程)调用返回的是Binder本地对象,远程(跨进程)调用返回的是BinderProxy代理对象。所有AIDL接口必须继承自IInterface。- Java层的
Binder类,代表的其实就是Binder本地对象。BinderProxy类它代表远程进程的Binder对象的本地代理;这两个类都继承自IBinder, 因而都具有跨进程传输的能力;实际上,在跨越进程的时候,Binder驱动会自动完成这两个对象的转换。IRemoteService代表的是远程server对象具有什么能力,是client与server端的调用契约(这里不用接口避免混淆)。具体来说,就是aidl里面的接口。同时因为它又继承自IInterface,所以它也具备检索与自身关联的Binder对象的能力。Stub是IRemoteService的静态内部类;这个类继承了Binder, 说明它是一个Binder本地对象,它实现了IRemoteService接口,表明它具有远程Server承诺给Client的能力;Stub是一个抽象类,具体的IRemoteService的相关实现需要我们手动完成,这里使用了策略模式。Proxy是Stub的静态内部类,与Stub不一样,虽然他们都既是Binder又是IInterface,不同的是Stub采用的是继承(is 关系),Proxy采用的是组合(has 关系)。他们均实现了所有的IInterface函数,不同的是,Stub又使用策略模式调用的是虚函数(待子类实现),而Proxy则使用组合模式。为什么Stub采用继承而Proxy采用组合?事实上,Stub本身is一个IBinder(Binder),它本身就是一个能跨越进程边界传输的对象,所以它得继承IBinder实现transact这个函数从而得到跨越进程的能力(这个能力由驱动赋予)。Proxy类使用组合,是因为他不关心自己是什么,它也不需要跨越进程传输,它只需要拥有这个能力即可,要拥有这个能力,只需要保留一个对IBinder的引用。因为它们都实现了IInterface,在Stub类里面,asBinder返回this,在Proxy里面返回的是持有的组合类IBinder的引用。(不懂的可以再看下系统为我们生成的IRemoteService源码)。
至此,你应该对AIDL这种通信方式里面的各个类以及各个角色有了一定的了解;它总是那么一种固定的模式:一个需要跨进程传递的对象一定继承自IBinder,如果是Binder本地对象,那么一定继承Binder实现IInterface,如果是代理对象,那么就实现了IInterface并持有了IBinder引用。
7 系统服务的IPC交互
通过上面的学习再去看系统的ActivityManagerService的源码,就知道哪一个类是什么角色了:IActivityManager是一个IInterface,它代表远程Service具有什么能力,ActivityManagerService继承自IActivityManager.Stub是Binder本地对象,对IActivityManager中所有能力做了实现,因此对于AMS的最终操作都会进入ActivityManagerService这个真正实现;IActivityManager.Stub里面有一个静态内部类Proxy, 它代表的就是Binder代理对象;是不是跟我们生成的AIDL模型一模一样呢?那么ActivityManager是什么?他不过是一个管理类而已,可以看到真正的操作都是转发给ActivityManagerService完成的。
1 | |
怎么样,上面代码熟不熟悉?ActivityManager内部的getService()方法实际上就是从ServiceManager中获取一个服务,这个服务实际就是AMS,然后通过IActivityManager.Stub.asInterface方法返回了一个IActivityManager。前面我们已经讲过,如果是本地调用这个IActivityManager实际上就是一个Binder本地对象这儿就是AMS,如果是远程调用IActivityManager就是Binder代理对象也就是AMS的代理对象。
这个地方肯定是跨进程调用,所以返回的是AMS的代理对象,而应用层ActivityManager就通过这个代理对象来和系统服务AMS进行各种IPC操作的。其他和系统服务的交互也类似,本文就不一一描述了。
参考资料: