前言
Android 系统为了安全、稳定性、内存管理等原因,Android 应用和系统服务都是运行在独立的进程中的,但系统服务与应用进程之间,应用进程A与应用进程B之间需要通信和数据共享的。因此,Android 系统需要提供一套能够高效、安全的跨进程通信方案。于是 Binder 就应运而生。
Binder 是 Android 中最重要的一种进程间通信机制,基于开源的 OpenBinder。George Hoffman 当时任 Be 公司的工程师,他启动了一个名为 OpenBinder 的项目,在 Be 公司被 ParmSource 公司收购后,OpenBinder 由 Dinnie Hackborn 继续开发,后来成为管理 ParmOS6 Cobalt OS 的进程的基础。在 Hackborn 加入谷歌后,他在 OpenBinder 的基础上开发出了 Android Binder(以下简称 Binder),用来完成 Android 的进程通信。
1 为什么需要学习 Binder
作为一名 Android 开发者,我们每天都在和 Binder 打交道,虽然可能有的时候不会注意到,譬如:
- startActivity 的时候,会获取 AMS 服务,调用 AMS 服务的 startActivity 方法
- startActivity 传递的对象为什么需要序列化
- bindService 为什么回调的是一个 IBinder 对象
- 多进程应用,各个进程之间如何通信
- AIDL 的使用
它们都和 Binder 有着密切关系,当碰到上面的场景,或者一些疑难问题的时候,理解 Binder 机制是非常有必要的。
2 为什么 Android 选择 Binder
这就要从进程间通信开始说起了,我们先看看比较常见的几种进程间通信方式。
2.1 常见进程间通信
2.1.1 共享内存
共享内存是进程间通信中最简单的方式之一,共享内存允许两个或更多进程访问同一块内存,当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改,它的原理如下图所示:
因为共享内存是访问同一块内存,所以数据不需要进行任何复制,是 IPC 几种方式中最快,性能最好的方式。但相对应的,共享内存未提供同步机制,需要我们手动控制内存间的互斥操作,较容易发生问题。同时共享内存由于能任意的访问和修改内存中的数据,如果有恶意程序去针对某个程序设计代码,很可能导致隐私泄漏或者程序崩溃,所以安全性较差。
2.1.2 管道
管道分为命名管道和无名管道,它是以一种特殊的文件作为中间介质,我们称为管道文件,它具有固定的读端和写端,写进程通过写段向管道文件里写入数据,读进程通过读段从读进程中读出数据,构成一条数据传递的流水线,它的原理如下图所示:
管道一次通信需要经历2次数据复制(进程A -> 管道文件,管道文件 -> 进程B)。管道的读写分阻塞和非阻塞,管道创建会分配一个缓冲区,而这个缓冲区是有限的,如果传输的数据大小超过缓冲区上限,或者在阻塞模式下没有安排好数据的读写,会出现阻塞的情况。管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。
2.1.3 消息队列
消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。消息队列允许多个进程同时读写消息,发送方与接收方要约定好,消息体的数据类型与大小。消息队列克服了信号承载信息量少、管道只能承载无格式字节流等缺点,消息队列一次通信同样需要经历2次数据复制(进程A -> 消息队列,消息队列 -> 进程B),它的原理如下图所示:
2.1.4 Socket
Socket 原本是为了网络设计的,但也可以通过本地回环地址 (127.0.0.1) 进行进程间通信,后来在 Socket 的框架上更是发展出一种 IPC 机制,名叫 UNIX Domain Socket。Socket 是一种典型的 C/S 架构,一个 Socket 会拥有两个缓冲区,一读一写,由于发送/接收消息需要将一个 Socket 缓冲区中的内容拷贝至另一个 Socket 缓冲区,所以 Socket 一次通信也是需要经历2次数据复制,它的原理如下图所示:
2.2 Binder
了解了常见进程间通信的方式,我们再来看一下 Binder 的原理。
Binder 是基于内存映射 mmap 设计实现的,我们需要先了解一下 mmap 的概念。
2.2.1 mmap
mmap 是一种内存映射的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
Linux 内核不会主动将 mmap 修改后的内容同步到磁盘文件中,有4个时机会触发 mmap 映射同步到磁盘:
- 调用 msync 函数主动进行数据同步(主动)
- 调用 munmap 函数对文件进行解除映射关系时(主动)
- 进程退出时(被动)
- 系统关机时(被动)
通过这种方式,直接操作映射的这一部分内存,可以避免一些数据复制,从而获得更好的性能。
2.2.2 原理
一次 Binder IPC 通信的过程分为以下几个步骤:
- 首先,Binder 驱动在内核空间中开辟出一个数据接收缓冲区
- 接着,在内核空间中开辟出一个内核缓冲区
- 将内核缓冲区与数据接收缓冲区建立映射关系
- 将数据接收缓冲区与接收进程的用户空间地址建立映射关系
- 发送方进程通过 copy_from_user 将数据从用户空间复制到内核缓冲区
- 由于内核缓冲区与数据接收缓冲区有映射关系,同时数据接收缓冲区与接收进程的用户空间地址有映射关系,所以在接收进程中可以直接获取到这段数据
这样便完成了一次 Binder IPC 通信,它的原理如下图所示:
可以看到,通过 mmap,Binder 通信时,只需要经历一次数据复制,性能要优于管道、消息队列、socket等方式,在安全性,易用性方面又优于共享内存。鉴于上述原因,Android 选择了这种折中的 IPC 方式,来满足系统对稳定性、传输性能和安全性方面的要求。
3 Binder 架构
Binder 在 Android 系统中江湖地位非常之高。在 Zygote 孵化出 system_server 进程后,在 system_server 进程中出初始化支持整个 Android framework 的各种各样的 Service,而这些 Service 从大的方向来划分,分为 Java 层 Framework 和 Native Framework 层(C++)的 Service,几乎都是基于 BInder IPC 机制。
Binder 也是一种 C/S 架构,分为 BpBinder(客户端)和 BBinder(服务端),他们都派生自 IBinder。其中 BpBinder 中的p表示 proxy,即代理。BpBinder 通过 transact 来发送事务请求,BBinder 通过 onTransact 来接收相应的事务。
Binder 一次通信的时序图如下:
Binder 采用分层架构设计:
4 总结
总之,一句话“无 Binder 不 Android”。