关键词:Binder / 跨进程通信机制 / AIDL /
Binder 是系统各个组件的桥梁,是一种极其方便的跨进程通信机制。Android 的四大组件、AMS、PMS 等系统服务都与 Binder 有关系。
这篇笔记是看《Android 开发艺术探索》之后,对 Binder 的进一步学习,对 Binder 知识的梳理参考了以下几篇非常优秀的文章(感谢存在那么多大神对知识的无私传播):(1)Weishu’s Notes - Binder 学习指南 (2)Android Binder 设计与实现 - 设计篇 (3)Android 进程间通信(IPC)机制 Binder 简要介绍和学习计划 (4)Android 跨进程通信之使用 AIDL 等等不一一列举。
1. AIDL #
AIDL (Android Interface Definition Language,安卓接口定义语言) 是一种 IDL 语言,用于生成可以在 Android 设备上两个进程之间进行进程间通信(interprocess communication, IPC)的代码。如果在一个进程中(例如 Activity)要调用另一个进程中(例如 Service)对象的操作,就可以使用 AIDL 生成可序列化的参数。AIDL IPC 机制是面向接口的,像 COM 或 Corba 一样,但是更加轻量级。它是使用代理类在客户端和实现端传递数据。
[ 关于 AIDL 需要知道的几点 ]
- 使用 AIDL 之前需要理解如何使用 bindService;
- 在 Android 上,一个进程不能正常访问另一个进程的内存,需要将它们的对象分解成操作系统所理解的基本单位,按次序跨越进程边界,AIDL 就起到这个作用;
- AIDL 允许你定义客户端与服务端达成一致的程序接口,使用进程间通信来相互交流;
- 什么时候使用 AIDL:
- 如果不需要执行不同应用之间的 IPC 并发,应该使用 Binder 建立接口;
- 如果想执行 IPC,但是不需要处理多线程,应该使用 Messenger 实现接口;
- 而使用 AIDL 的必要情况是:允许来自不同应用的客户端跨进程通信来访问你的 Service,而且想要在你的 Service 中处理多线程;
- AIDL 的原理:实际上就是通过我们写的 aidl 文件,帮助我们生成了一个接口,一个 Stub 类用于服务端,一个 Proxy 类用于客户端调用。
我们使用AIDL接口的时候,经常会接触到 IBinder / IInterface / Binder / BinderProxy / Stub 这些类,那么这每个类代表的是什么呢?
IBinder
是一个接口,它代表了一种跨进程传输的能力;只要实现了这个接口,就能将这个对象进行跨进程传递;这是驱动底层支持的;在跨进程数据流经驱动的时候,驱动会识别 IBinder 类型的数据,从而自动完成不同进程的 Binder 本地对象以及 Binder 代理对象的转换。IBinder 负责数据传输。- 这里的
IInterface
代表的就是远程 server 对象具有什么能力。具体来说,就是 aidl 里面的接口。 - Java 层的
Binder
类,代表的其实就是 Binder 本地对象。 BinderProxy
类是 Binder 类的一个内部类,它代表远程进程的 Binder 对象的本地代理;这两个类都继承自 IBinder, 因而都具有跨进程传输的能力;实际上,在跨越进程的时候,Binder 驱动会自动完成这两个对象的转换。- 在使用 AIDL 的时候,编译工具会给我们生成一个
Stub
的静态内部类;这个类继承了 Binder, 说明它是一个 Binder 本地对象,它实现了 IInterface 接口,表明它具有远程 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现需要我们手动完成,这里使用了策略模式。
它总是那么一种固定的模式:一个需要跨进程传递的对象一定继承自 IBinder,如果是 Binder 本地对象,那么一定继承 Binder 实现 IInterface,如果是代理对象,那么就实现了 IInterface 并持有了 IBinder 引用;
Proxy 与 Stub 不一样,虽然他们都既是 Binder 又是 IInterface,不同的是 Stub 采用的是继承(is 关系),Proxy 采用的是组合(has 关系)。
2. 内核模块 / 驱动 #
- 用户空间访问内核空间的唯一方式是系统调用。
- 当一个进程执行系统调用而陷入内核代码中执行,该进程便处于内核运行态(内核态)。当进程只是在执行用户自己的代码时,是处于用户运行态(用户态)。
- 传统的 Linux 通信机制比如 Socket、管道等都是内核支持的,但 Binder 不是 Linux 内核的一部分,需要依靠 Linux 的 动态可加载内核模块 (Loadable Kernel Module, LKM)机制来解决这个问题。模块具有独立功能,可以被单独的编译,但是不能独立运行,需要在运行的时候被链接到内核作为内核的一部分在内核空间运行。
- Android 系统通过 Binder 通信的内核模块,即 Binder 驱动,运行在内核空间,用户进程之间通过这个模块作为桥梁完成需要的通信。(驱动就是操作硬件的接口)
3. Binder 通信模型 #
- Android 特别使用 Binder 的原因:性能 + 安全。移动设备广泛地使用跨进程通信,需要对通信机制本身有更严格的要求。传统的进程通信方式对于通信双方的身份没有严格的验证,仅仅是在上层协议进行架设,比如 Socket 通信 ip 地址是客户端手动填入的,可以被伪造。Binder 机制从协议本身就支持对通信双方作身份校验,提升安全性。(PS:这也是 Android 权限模型的基础)
- 进程的隔离实现,使用了虚拟地址空间。由于进程隔离机制的存在,所以跨进程通信的双方(Server 进程,Client 进程)无法通过简单的方式进行通信,需要 Binder 机制。
- 两个运行在用户空间的进程要完成通信,必须借助内核的帮助,运行在内核里面的程序叫做 Binder驱动,类似于一次打电话过程中的基站的作用;而还有一个很重要的东西叫做 ServiceManager,相当于通信录的作用。
Binder 通信模型有四个角色(Client / Server / ServiceManager / Driver),通信步骤大致如下:
- 有一个进程向驱动申请为 ServiceManager,驱动同意之后,它便可以负责对 Service 的管理;
- Server 向 ServiceManager 注册,每一个 Service 端的进程启动之后要向 ServiceManager 报告,ServiceManager 建立一张表,对应着各个 Server 的名字和地址;
- Client 询问 ServiceManager 如何联系到想要联系的 Server,ServiceManager 返回一个消息,Client 根据这个消息就可以开始建立与 Server 的通信;
这里 Client 与 ServiceManager 的通信,Client 与 Server 的通信都会经过驱动,驱动是整个过程的核心;
两个运行在用户空间的进程 A 和进程 B 如何完成通信?有一种方案:
内核可以访问 A 和 B 的所有数据;所以,可以通过内核做中转;假设进程 A 要给进程 B 发送数据,那么就先把 A 的数据 copy 到内核空间,然后把内核空间对应的数据 copy 到 B 就完成了;用户空间要操作内核空间,需要通过系统调用;刚好,这里就有两个系统调用:copy_from_user, copy_to_user。
但是但是但是,Binder 机制并不是这么做的!!!看图(该图来自Binder 学习指南)
- 首先,Server 进程要向 ServiceManager 注册,告诉 ServiceManager 一个标识之类的信息,于是 ServiceManager 建立了一张表:这个标识对应进程这个 Server;
- 然后,Client 向 ServiceManager 查询,告诉 ServiceManager 它需要这个标识信息对应的某个内容,驱动在数据流过的时候返回 Client 所需要的对象(这其实不是原本真正的对象,而是一个一模一样的代理对象,而且这个对象的内容什么也不做,只是直接把参数做了一些包装直接转发给了 Binder 驱动)
- 对于驱动而言,通过查表发现用的是什么代理对象替换了原本的对象给 Client,然后去正真的访问真对象,并且告诉 Server 把调用的结果发送给驱动,驱动把结果返回给 Client 进程。
由于驱动返回的 objectProxy 与 Server 里的原始的 object 近乎一样,给人感觉是直接 “ 把 Server 进程里面的对象 Object 传递给了 Client 进程 ”,我们因此说是 “ Binder 对象是可以进行跨进程传递的一种对象 ”。
其实不然,Binder 跨进程传输并不是真的把一个对象传输给了另一个进程;传输过程好像是 Binder 跨进程穿越的时候玩了一个魔术,它在一个进程留下了一个真身,在另一个进程幻化出一个影子(这个影子可以有多个);所以说!!! Client 进程的操作实际上是对于影子的操作,影子利用 Binder 驱动最终让真身完成操作。
Android 系统实现这种机制使用的是 代理模式
, 对于 Binder 的访问,如果是在同一个进程(不需要跨进程),那么直接返回原始的 Binder 实体;如果在不同进程,那么就给他一个代理对象(影子);我们在系统源码以及 AIDL 的生成代码里面可以看到很多这种实现。
PS:Server 进程向 ServiceManager 注册的过程也是跨进程通信,驱动也会对这个过程进行暗箱操作:ServiceManager 中存在的 Server 端的对象实际上也是代理对象,后面 Client 向 ServiceManager 查询的时候,驱动会给 Client 返回另外一个代理对象。Server 进程的本地对象仅有一个,其他进程所拥有的全部都是它的代理。
一句话总结就是:Client 进程只不过是持有了 Server 端的代理;代理对象协助驱动完成了跨进程通信。
4. 关于 Binder 所需要知道的 #
- Binder 的设计采用了抽象的面向对象的思想,对于 Binder 通信的使用者而言, Server 里面的 Binder 和 Client 里面的 Binder 没有什么不同,一个 Binder 代表了所有,不用关心实现的细节,甚至不用关心驱动以及 ServiceManager 的存在;
- Binder 指的是一种通信机制,IPC 机制;
- 对于 Server 进程来说,Binder 指的是 Binder 本地对象;
- 对于 Client 来说,Binder 指的是 Binder 代理对象,只是 Binder 对象的一个远程代理,需要明白的是,对这个 Binder 代理对象的操作,会通过驱动最终转发到 Binder 本地对象上去完成。而对于使用者来说,并不需要关心到底是 Binder 的代理对象还是本地对象,对两者的操作没有任何区别;
- Binder 是可以进行跨进程传递的对象,Binder 驱动会对具有跨进程传递能力的对象做特殊处理:自动完成代理对象和本地对象的转换;
- Binder 的实体(本地对象)位于一个进程中,而它的引用(代理对象)却遍布于系统的各个进程之中。
- Binder 模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的 Binder 对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是 Binder 在英文里的原意。
End.
Note by HF.
Learn from Internet & 《Android 开发艺术探索》