Binder 是什么?

Binder 是一种进程间通信机制,基于开源的 OpenBinder 实现;OpenBinder 起初由 Be Inc. 开发,后由 Plam Inc. 接手,现在 OpenBinder 的作者在 Google 工作,这也为后来的 Binder 。从字面上来解释 Binder 有胶水、粘合剂的意思,顾名思义就是粘和不同的进程,使之实现通信。

Binder 有那么重要吗 ?

作为一个 Android 攻城狮,会不会有这也的疑惑?

  • 为什么 Activity 间传递对象需要序列化?
  • Activity 的启动流程是什么样的?
  • 四大组件底层的通信机制是怎样的?
  • AIDL 内部的实现原理是什么?
  • 插件化编程技术应该从何学起?

这些都是因为 Binder 才让无数 Android 小伙伴疑惑重重,想要知道 Android 中血液是如何流动的,就必须正确认识 Binder

为什么选中了 Binder 实现 IPC 机制?

Android 内核是基于 Linux 系统, 而 Linux 现存多种进程间 IPC(Inter-Process Communication) 方式

  • 管道:在创建时分配一个 page 大小的内存,管道是由内核管理的一个缓冲区,缓存区大小比较有限;
  • 消息队列:信息复制两次,额外的 CPU 消耗;不合适频繁或信息量大的通信;
  • 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
  • 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
  • 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 信号:不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;

为何不直接采用 Linux 现有的进程 IPC 方案呢,难道 Linux 社区那么多优秀人员都没有考虑到有 Binder 这样一个更优秀的方案,还是 google 太过于牛 B

这得从各个方面考虑到

  1. 性能角度(数据拷次数): Binder 数据拷贝只需要一次,而管道、消息队列、Socket 都需要 2 次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder 性能仅次于共享内存。

  2. 稳定性: Binder是基于 C/S 架构的,C/S 架构,是指客户端 (Client) 和服务端 (Server) 组成的架构,Client 端有什么需求,直接发送给 Server 端去完成,架构清晰明朗,Server 端与 Client 端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder 架构优越于共享内存。

  3. 安全性: Android 作为一个开放性的平台,市场上有各类海量的应用供用户选择安装,因此安全性对于 Android 平台而言极其重要。对于我们普通用户,绝不希望从 App 商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统 Linux IPC 无任何保护措施,完全由上层协议来确保。

    传统 Linux IPC 的接收方无法获得对方进程可靠的 UID/PID ,从而无法鉴别对方身份;而 Android 为每个安装好的应用程序分配了自己的 UID ,故进程的 UID 是鉴别进程身份的重要标志;传统 IPC 只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由 IPC 机制本身在内核中添加。其次传统 IPC 访问接入点是开放的,无法建立私有通道。从安全角度,Binder 的安全性更高。

    但是 Android 就算用了 Binder 架构,而现如今 Android 手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说 Binder 的安全性不好,因为 Android 系统仍然是掌握主控权,可以控制这类 App 的流氓行为,只是对于该采用何种策略来控制,在这方面 android 的确存在很多有待进步的空间,这也是 google 以及各大手机厂商一直努力改善的地方之一。

Linux 传统 IPC 如何通信的?

了解 Linux IPC 相关的概念和原理有助于理解 Binder 通信原理。因此,在介绍 Binder 跨进程通信原理之前,先聊聊 Liunx 中的几个概念以及 Linux 系统下传统的进程间通信是如何实现。

图片来源于《写给 Android 应用工程师的 Binder 原理剖析》

进程隔离

操作系统中进程隔离指的是,进程与进程间内存是不共享的,也就是一个进程不能直接操作或者访问另一个进程,A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。

进程空间划分:用户空间(User Space)/内核空间(Kernel Space)

现在操作系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 232 次方,也就是 4GB

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限。Liunx为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间和内核空间。针对 Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。

内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。每个进程可以通过系统调用进入内核,因此,Linux 内核对系统内的所有进程共享。

图片来源于《用户空间与内核空间,进程上下文与中断上下文总结》

系统调用

系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。

当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)

系统调用主要通过如下两个函数来实现:

1
2
copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间

Linux 的 IPC 通信原理

图片来源于《写给 Android 应用工程师的 Binder 原理剖析》

  1. 消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。
  2. 然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。
  3. 接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。

LinuxIPC 通信原理有两个问题:

  • 一次数据传递需要经历:用户空间 –> 内核缓存区 –> 用户空间,需要 2 次数据拷贝,这样效率不高。
  • 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用API接收消息头来获取消息体的大小,浪费了空间或者时间。

Binder IPC 如何通信的?

跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?

这就得益于 Linux动态内核可加载模块Loadable Kernel Module,LKM)的机制;

  • 模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。
  • 它在运行时被链接到内核作为内核的一部分运行。

这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。(在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动Binder Dirver)。)

Binder 是如何做到减少数据拷贝的呢?

这就得说一下 Linux 下的另一个概念:内存映射

Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法,通常是用在有物理介质的文件系统上的。

比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);

通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。这样就可以减少数据拷贝次数,用内存读写取代 I/O 读写,提高文件读取效率,实现用户空间和内核空间的高效互动。

两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。

但是 Binder 并不存在物理介质!

因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间

  1. 这样 Binder 驱动在内核空间创建一个数据接收缓存区;
  2. 接着在内核空间开辟一块内核缓存区,建立内核缓存区内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区接收进程用户空间地址的映射关系;
  3. 发送方进程通过系统方法将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

图片来源于《写给 Android 应用工程师的 Binder 原理剖析》

深入了解

  • Binder 设备的初始化过程

Binder 设备的初始化过程是在Binder 驱动程序的初始化函数 binder_init 中进行的。

它首先在目标设备上创建了一个 /proc/binder/proc 目录,每一个使用了 Binder 进程间通信机制的进程在该目录下都对应有一个文件,这些文件是以进程 ID 来命名的,通过它们就可以读取到各个进程的 Binder 线程池、Binder 实体对象、Binder 引用对象以及内核缓冲区等信息。然后调用函数 misc_register 来创建一个 Binder 设备,最后 Binder 驱动程序在目标设备上创建了一个 Binder 设备文件 /dev/binder,这个设备文件的操作方法列表是由全局变量 binder_fops 指定的。

全局变量 binder_fopsBinder 设备文件 /dev/binder 指定文件打开 (binder_open),映射(binder_mmap),数据操作(binder_ioctl)

  • binder_open 打开过程

一个进程在使用 Binder 进程间通信机制之前,首先要调用函数 open 打开设备文件 /dev/binder 来获得一个文件描述符,然后才能通过这个文件描述符来和 Binder 驱动程序交互,继而和其他进程执行 Binder 进程间通信。

当进程调用函数 open 打开设备文件 /dev/binder 时,Binder 驱动程序中的函数 binder_open 就会被调用,它会为进程创建一个 binder_proc 结构体 proc,并把它加入到一个全局 hash 队列 binder_procs 中。Binder 驱动程序将所有打开了设备文件 dev/binder 的进程都加入到全局 hash 队列 binder_procs 中。最后会在目标设备上的 /proc/binder/proc 目录下创建一个以进程 ID 为名称的只读文件,通过读取这个文件就可以获得进程 PIDBinder 线程池、Binder 实体对象、Binder 引用对象以及内核缓冲区等信息。

  • 内存映射过程

进程打开了设备文件 /dev/binder 之后,还必须要调用函数 mmap 把这个设备文件映射到进程的地址空间,然后才可以使用 Binder 进程间通信机制。设备文件 /dev/binder 对应的是一个虚拟设备,将它映射到进程的地址空间的目的并不是对它的内容感兴趣,而是为了为进程分配内核缓冲区,以便它可以用来传输进程间通信数据。

当进程调用函数 mmap 将设备文件 /dev/binder 映射到自己的地址空间时,Binder 驱动程序中的函数 binder_mmap 就会被调用。

Binder 驱动程序最多可以为进程分配 4M 内核缓冲区来传输进程间通信数据。Binder 驱动程序为进程分配的内核缓冲区在用户空间只可以读,而不可以写。

Binder 驱动程序为进程分配的内核缓冲区有两个地址,其中一个是用户空间地址,另一个是内核空间地址。进程通过用户空间地址来访问这块内核缓冲区的内容,而 Binder 驱动程序通过内核空间地址来访问这块内核缓冲区的内容。由于它们是连续的,并且起始地址相差一个固定值,因此,只要知道其中的一个地址,就可以方便的计算出另外一个地址。

Binder 驱动程序为进程分配的内核缓冲区即为一系列物理页面,它们分别被映射到进程的用户地址空间和内核地址空间。当 Binder 驱动程序需要将一块数据传输给一个进程时,它就可以先把这块数据保存在为该进程所分配的一块内核缓冲区中,然后再把这块内核缓冲区的用户空间地址告诉进程,最后进程就可以访问到里面的数据了。这样做的好处便是不需要将数据从内核空间复制到用户空间,从而提高了数据的传输效率。

了解了 Binder IPC 的底层通信原理,再看看实现层面 Binder 架构是如何设计的。

Binder 通信模型

一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程 (Client) 和服务端进程 (Server) ,由于进程隔离机制的存在,通信双方必然需要借助 Binder 来实现。

从组件视角来说,Binder 包含 ClientServerServiceManager 以及 binder驱动 ,其中ServiceManager 用于管理系统中的各种服务

图片来源于《彻底理解 Android Binder 通信架构》

其中 Client(AMP)Server(AMS)Service Manager 运行在用户空间,它们之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder Driver 进行交互的,Binder 驱动运行在内核空间。其中 Service ManagerBinder 驱动由系统提供是 Android 平台的基础架构,而 ClientServer 由应用程序来实现。ClientServerServiceManager 均是通过系统调用 openmmapioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。( 此处的 Service Manager 是指 Native 层的 ServiceManager(C++),并非指 framework 层的 ServiceManager(Java) )

ClientServerServiceManagerBinder 驱动这几个组件在通信过程中扮演的角色就如同 互联网中服务器 (Server)、客户端 (Client)DNS 域名服务器 (ServiceManager) 以及路由器 (Binder 驱动) 之前的关系。

通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如 google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能访问到 google.com 对应的服务器。

图片来源于《写给 Android 应用工程师的 Binder 原理剖析》

  • Binder 驱动

    Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

  • ServiceManager

    ServiceManagerDNS 类似,作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用。

    • 实名 Binder

      注册了名字的 Binder 叫实名 Binder,就像网站一样除了除了有 IP 地址以外还有自己的网址。

如何注册服务?

Server 创建了 Binder,并为它起一个字符形式,可读易记的名字,将这个 Binder 实体连同名字一起,以数据包的形式通过 Binder 驱动发送给 ServiceManager ,通知 ServiceManager 注册一个名为 “xx”Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder ,创建位于内核中的实体节点,以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager

但是!这里有个问题! ServierManager 是一个进程,Server 是另一个进程,ServerServiceManager 中注册 Binder 必然涉及到进程间通信。我们在实现进程间通信的时候需要先用到进程间通信,这… 这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!

解决方法Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManagerServer 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。

ServiceManger 收到数据后从中 取出名字和引用 填入查找表。

ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDER_SET_CONTEXT_MGR 命令将自己注册成 ServiceManager 时,Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。这个 Binder 实体的引用在所有 Client 中都固定为 0, 无需通过其它手段获得。

也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManagerBinder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。还有需要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client

怎么获取服务?

ServerServiceManager 中注册了 Binder 以后,Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder 我申请访问名字叫 “xx”Binder 引用。

ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的 Client

从面向对象的角度看,Server 中的 Binder 实体现在有两个引用:一个位于 ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样。

Binder 通信过程

  1. 首先,一个进程使用 BINDER_SET_CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager
  2. Server 通过驱动向 ServiceManager 中注册 BinderServer 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点,以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManagerServiceManger 将其填入查找表。
  3. Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。

图片来源于《写给 Android 应用工程师的 Binder 原理剖析》

Binder 通信中的代理模式

我们已经解释清楚 ClientServer 借助 Binder 驱动完成跨进程通信的实现机制了,但是还有个问题会让我们困惑。

A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object

一次通信过程简述

跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。

A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。

Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。

Binder 中的具体调用如下:

Client 进程和 Server 进程的一次通信过程中,涉及了四种类型的对象,它们分别是

  • 位于 Binder 驱动程序中的 Binder 实体对象 (binder_node)Binder 引用对象 (binder_ref)
  • 位于 Binder 库中的 Binder 本地对象 (BBinder)Binder 代理对象 (BpBinder)

它们的交互过程如下:

  1. 运行在 Client 进程中的 Binder 代理对象通过 Binder 驱动程序向运行在 Server 进程中的 Binder 本地对象发出一个进程间通信请求,Binder 驱动程序接着就根据 Client 进程传递过来的 Binder 代理对象的句柄值来找到对应的 Binder 引用对象
  2. Binder 驱动程序根据前面找到的 Binder 引用对象找到对应的 Binder 实体对象,并且创建一个事务 (binder_transaction) 来描述该次进程间通信过程。
  3. Binder 驱动程序根据前面找到的 Binder 实体对象来找到运行在 Server 进程中的 Binder 本地对象,并且将 Client 进程传递过来的通信数据发送给它处理。
  4. Binder 本地对象处理完成 Client 进程的通信请求之后,就将通信结果返回给 Binder 驱动程序,Binder 驱动程序接着就找到前面所创建的一个事务。
  5. Binder 驱动程序根据前面找到的事务的相关属性来找到发出通信请求的 Client 进程,并且通知 Client 进程将通信结果返回给对应的 Binder 代理对象处理。

图片来源于《写给 Android 应用工程师的 Binder 原理剖析》

从这个过程就可以看出,Binder 代理对象依赖于 Binder 引用对象,而 Binder 引用对象又依赖于 Binder 实体对象,最后,Binder 实体对象又依赖于 Binder 本地对象。这样,Binder 进程间通信机制就必须采用一种技术措施来保证,不能销毁一个还被其他对象依赖着的对象。为了维护这些 Binder 对象的依赖关系,Binder 进程间通信机制采用了引用计数来维护每一个 Binder 对象的生命周期。

深入了解

  • Binder 本地对象的生命周期

Binder 本地对象是一个类型为 BBinder 的对象,它是在用户空间中创建的,并且运行在 Server 进程中。

Binder 本地对象一方面会被运行在 Server 进程中的其他对象引用,另一方面也会被 Binder 驱动程序中的 Binder 实体对象引用。

由于 BBinder 类继承了 RefBase 类,因此,Server 进程中的其他对象可以简单的通过智能指针来引用这些 Binder 本地对象,以便可以控制它们的生命周期。

由于 Binder 驱动程序中的 Binder 实体对象是运行在内核空间的,它不能够通过智能指针来引用运行在用户空间的 Binder 本地对象,因此,Binder 驱动程序就需要和 Server 进程约定一套规则来维护它们的引用计数,避免它们在还被 Binder 实体对象引用的情况下销毁。

如何保证本地对象在 Binder 实体对象引用的情况下不被销毁?

Server 进程将一个 Binder 本地对象注册到 ServerManager 时,Binder 驱动程序就会为它创建一个 Binder 实体对象。接下来,当 Client 进程通过 ServerManager 来查询一个 Binder 本地对象的代理对象接口时,Binder 驱动程序就会为它所对应的 Binder 实体对象创建一个 Binder 引用对象,接着在使用 BR_INCREFSBR_ACQUIRE 协议来通知对应的 Server 进程增加对应的 Binder 本地对象的弱引用技术和强引用技术。这样就能保证 Client 进程中的 Binder 代理对象在引用一个 Binder 本地对象期间,该 Binder 本地对象不会被销毁。当没有任何 Binder 代理对象引用一个 Binder 本地对象时,Binder 驱动程序就会使用 BR_DECREFSBR_RELEASE 协议来通知对应的 Server 进程减少对应的 Binder 本地对象的弱引用技术和强引用技术。

总结来说,Binder 驱动程序就是通过 BR_INCREFSBR_ACQUIREBR_DECREFSBR_RELEASE 协议来引用运行在 Server 进程中的 Binder 本地对象的,相关的代码实现在函数 binder_thread_read 中。

  • Binder 实体对象的生命周期

Binder 实体对象是一个类型为 binder_node 的对象,它是在 Binder 驱动程序中创建的,并且被 Binder 驱动程序中的 Binder 引用对象所引用。

Client 进程第一次引用一个 Binder 实体对象时,Binder 驱动程序就会在内部为它创建一个 Binder 引用对象。例如,当 Client 进程通过 ServerManager 来获得一个 Service 组件的代理对象接口时,Binder 驱动程序就会找到与该 Service 组件对应的 Binder 实体对象,接着再创建一个 Binder 引用对象来引用它。这时候就需要增加被引用的 Binder 实体对象的引用计数。相应地,当 Client 进程不再引用一个 Service 组件时,它也会请求 Binder 驱动程序释放之前为它所创建的 Binder 引用对象。这时候就需要减少该 Binder 引用对象所引用的 Binder实体对象的引用计数。

  • Binder 引用对象的生命周期

Binder 引用对象是一个类型为 binder_ref 的对象,它是在 Binder 驱动程序中创建的,并且被用户空间中的 Binder 代理对象所引用。

Client 进程引用了 Server 进程中的一个 Binder 本地对象时,Binder 驱动程序就会在内部为它创建一个 Binder 引用对象。由于 Binder 引用对象是运行在内核空间的,而引用了它的 Binder 代理对象是运行在用户空间的,因此,Client 进程和 Binder 驱动程序就需要约定一套规则来维护 Binder 引用对象的引用计数,避免它们在还被 Binder 代理对象引用的情况下被销毁。

这套规则可以划分为 BC_ACQUIREBC_INCREFSBC_RELEASEBC_DECREFS 四个协议,分别用来增加和减少一个 Binder 引用对象的强引用技术和弱引用技术。相关的代码实现在 Binder 驱动程序的函数binder_thread_write 中。

  • Binder 代理对象的生命周期

Binder 代理对象是一个类型为 BpBinder 的对象,它是在用户空间中创建的,并且运行在 Client 进程中。

Binder 本地对象类似,Binder 代理对象一方面会被运行在 Client 进程中的其他对象引用,另一方面它也会引用 Binder 驱动程序中的 Binder 引用对象。

由于 BpBinder 类继承了 RefBase 类,因此,Client 进程中的其他对象可以简单地通过智能指针来引用这些 Binder 代理对象,以便可以控制它们的生命周期。

由于 Binder 驱动程序中的 Binder 引用对象是运行在内核空间的,Binder 代理对象就不能通过智能指针来引用它们,因此,Client 进程就需要通过 BC_ACQUIREBC_INCREFSBC_RELEASEBC_DECREFS 四个协议来引用 Binder 驱动程序中的 Binder 引用对象。

前面提到,每一个 Binder 代理对象都是通过一个句柄值来和一个 Binder 引用对象关联的,而 Client 进程就是通过这个句柄值来维护运行在它里面的 Binder 代理对象的。具体来说,就是 Client 进程会在内部创建一个 handle_entry 类型的 Binder 代理对象列表,它以句柄值作为关键字来维护它内部所有的 Binder 代理对象。

  • 智能指针

智能指针是一种能够自动维护对象引用计数的技术,它是一个对象而不是一个指针,但是它引用了一个实际使用的对象。正是因为它是一个对象,因此它能够自动地维护实际对象的引用计数。简单来说,就是在智能指针构造时,增加它所引用的对象的引用计数;而在智能指针析构时,就减少它所引用的对象的引用计数。由于智能指针的构造和析构都是自动的,因此,它就很自然的实现了自动的对象引用计数技术。

但是这样并不能解决对象的相互引用问题,于是就需要采取一种稍微复杂的引用计数技术来维护对象的生周期了,这种引用计数技术将对象的引用计数分为强引用计数和弱引用计数两种,其中,对象的生命周期只受强引用计数控制。

Android 系统提供了三种类型的 C++ 智能指针,分别为轻量级指针(Light Pointer)、强指针(Strong Pointer)和弱指针(Weak Pointer)。

Binder 的完整定义

  • 从进程间通信的角度看,Binder 是一种进程间通信的机制;
  • Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
  • Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
  • 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。

简单 Demo

HelloBinder

感谢:本文收集整理自如下文章

写给 Android 应用工程师的 Binder 原理剖析

为什么 Android 要采用 Binder 作为 IPC 机制?

Binder 对象引用计数技术

Binder 设备文件的初始化、打开和内存映射过程

Android Binder原理(一)学习Binder前必须要了解的知识点

彻底理解Android Binder通信架构

Binder系列

可能问到的问题

  • 进程间通信的方式?
  • Binder机制的作用和原理?
    • 上文 Binder IPC 如何通信?
  • 简述IPC?
  • 什么是AIDL?如何使用?
    • AIDL(Android Interface Define Language)IPC 进程间通信方式的一种,用于生成可以在 Android 设备上两个进程之间进行进程间通信(interprocess communication, IPC) 的代码.
    • AIDL使用解析
    • Android中AIDL的使用详解
  • AIDL解决了什么问题?
    • 官方文档: Note: Using AIDL is necessary only if you allow clients from different applications to access your service for IPC and want to handle multithreading in your service. If you do not need to perform concurrent IPC across different applications, you should create your interface by implementing a Binder or, if you want to perform IPC, but do not need to handle multithreading, implement your interface using a Messenger. Regardless, be sure that you understand Bound Services before implementing an AIDL. “只有当你允许来自不同的客户端访问你的服务并且需要处理多线程问题时你才必须使用AIDL”
  • Android 进程分类?
  • 谈谈对进程共享和线程安全的认识?