AIDI 和 Binder
一、概述
Binder 可以提供系统中任何程序都可以访问的全局服务。Android Binder 框架分为服务器接口、Binder 驱动以及客户端接口
- 服务端接口:实际上是 Binder 类的对象,该对象一旦创建,内部则会启动一个隐藏线程,会接收 Binder 驱动发送的消息,收到消息后,会执行 Binder 对象中的
onTransact()
函数,并按照该函数的参数执行不同的服务端代码; - Binder 驱动:该对象也为 Binder 类的实例,客户端通过该对象访问远程服务。
- 客户端接口:获得 Binder 驱动,调用其
transact()
发送消息至服务器
二、AIDL 的使用
创建 aidl 文件;创建一个 Service;利用 XxxAIDL.Stub 通过onBind()
返回 Binder 给客户端,暴露接口;客户端通过 ServiceConnection 与之交互。
服务端
创建一个包名:com.demo.aidl,在包内创建一个 ICalcAIDL.aidl 文件:
1 | package com.demo.aidl.calc |
1 | package com.demo.hg_binder; |
再在 AndroidManifest 中注册
1 | <service android:name="com.demo.hg_binder.CalcService"> |
客户端
1 | public class MainActivity extends Activity{ |
分析 AIDL 生成的代码
Stub 类 onTransact()
1 | public static abstract class Stub extends android.os.Binder implement com.demo.aldl.calc{ |
code 是一个整型的唯一标识,用于区分执行哪个方法,客户端会传递此参数,告诉服务端执行哪个方法;data 是客户端传递过来的参数;reply 是服务端返回给客户端的值;flags 标明是否有返回值,0 为有(双向),1 为没有(单向)
Binder 原理剖析
为什么必须理解 Binder ?
- 为什么 Activity 间传递对象需要序列化?
- Activity 的启动流程是什么样的?
- 四大组件底层的通信机制是怎样的?
- AIDL 内部的实现原理是什么?
- 插件化编程技术应该从何学起?……
为什么是 Binder
性能、稳定性和安全性
Linux 下传统的进程间通信原理
- 进程隔离:内存是不共享的
- 进程空间划分:用户空间(User Space)、内核空间(Kernel Space)。它们之间是隔离的
- 系统调用:用户态与内核态
通常的做法是将消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用copyfromuser()
将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用copytouser()
将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间的通信。
存在两个问题:
- 性能低下
- 接收数据的缓存区大小无法知道,只能尽可能大,浪费空间或是时间。
Binder 跨进程通信原理
动态内核可加载模块 && 内存映射
跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分。Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核加载模块(Loadable Kernel Module, LKM)的机制。模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Driver)。
如何通过这个内核模块(Binder Driver)来实现通信的呢?—— 内存映射。
内存机制简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反映到内核空间,反之内核空间对这段区域的修改也能直接反映到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动,各自空间的修改能被对方及时感知。
Binder IPC 机制中涉及到的内存映射通过mmap()
来实现。
Binder IPC 实现原理
mmap()
通常是用在有物理介质的文件系统上的。比如进程中互动用户区域是不能直接和物理设备打交道的,如果要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘->内核空间->用户空间)。通常这种场景下mmap()
就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据拷贝次数,用内存读写取代 I/O 读写,提高文件读取效率。
而 Binder 并不存在物理介质,因此 Binder 驱动使用mmap()
并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。
- 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用
copyfromuser()
将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间通信。
Binder 通信模型
Client/Server/ServiceManager/驱动
Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中的服务器(Server)、客户端(Client)、DNS 域名服务器(ServiceManager)以及路由器(Binder 驱动)之间的关系。
Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
ServiceManager 与实名 Binder。ServiceManager 和 DNS 类似作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用。注册了名字的 Binder 叫 实名 Binder,就像网站一样除了有 IP 地址还有自己的域名。
Server 创建了 Binder,并为他起一个字符形式,可读易记的名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager,通知 ServiceManager 注册一个名为所命名的 Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将这个以及新建的引用打包传给 ServiceManager,ServiceManager 收到数据后从中取出名字和引用填入查找表。
ServiceManager 是一个进程,Server 是另外一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信?!ServiceManager 和其他进程同样采用 Binder 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册、查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDER_SET_CONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其他手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client
Server 向 ServiceManager 中注册了 Binder 以后,Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder,ServiceManager 收到请求后从请求数据包中取出 Binder 名称,在查找表中找到对应的条目,取出对应的 Binder 引用作为回复发送回发起请求的 Client。
Binder 通信过程
- 一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
- Server 通过驱动向 ServiceManager 中注册 Binder (Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManager 将其填入查找表。
- Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
Binder 通信中的代理模式
当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一模一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把请求参数交给驱动就可以。对于 A 进程来说和直接调用 object 中的方法是一样的。
当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。