[置顶] 深入理解android之IPC机制与Binder框架

时间:2023-03-08 16:37:08

[android之IPC机制与Binder框架]

[Binder框架、Parcel、Proxy-Stub以及AIDL]

Abstract

[每个平台都会有自己一套跨进程的IPC机制,让不同进程里的两个程序之间能够互相沟通,在Android世界中,Binder框架提供作为沟通管道的IPC接口是IBinder。另外,由于采用了新的IPC机制,必然要求使用性能更出色的对象传输方式。在这样的环境下,Parcel被设计出来,其定位就是轻量级的高效的对象序列化和反序列化机制。]

摘要

Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有管道,system V IPC,socket等IPC手段,却还要倚赖Binder来实现进程间通信,说明Binder具有无可比拟的优势。深入了解Binder并将之与传统IPC做对比有助于我们深入领会进程间通信的实现和性能优化。本文将对Binder的设计细节做一个全面的阐述,首先通过介绍Binder通信模型和Binder通信协议了解Binder的设计需求;然后分别阐述Binder在系统不同部分的表述方式和起的作用;最后还会解释Binder在数据接收端的设计考虑,包括线程池管理,内存映射和等待队列管理等。通过本文对Binder的详细介绍以及与其它IPC通信方式的对比,对Binder的优势和使用Binder作为Android主要IPC方式的原因有深入了解。

第0章 Binder设计与实现

1. 设计需求

一方面是传输性能。socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。

表 1 各种IPC方式数据拷贝次数

IPC                数据拷贝次数

共享内存               0

Binder                  1

Socket/管道/消息队列    2

一方面是出于安全性考虑。Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

2. 面向对象的 Binder IPC

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问server。在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端Server中,而后者实体位于本地内存中。‘指针’是C++的术语,而更通常的说法是引用,即Client通过Binder的引用访问Server。而软件领域另一个术语‘句柄’也可以用来表述Binder在Client中的存在方式。从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。本文中会使用‘引用’或‘句柄’这个两广泛使用的术语。

面向对象思想的引入将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。最诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的Binder对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是Binder在英文里的原意。

当然面向对象只是针对应用程序而言,对于Binder驱动和内核其它模块一样使用C语言实现,没有类和对象的概念。Binder驱动为面向对象的进程间通信提供底层支持。

3.Binder 通信模型

Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

3.1Binder 驱动

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。

3.2ServiceManager 与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

细心的读者可能会发现其中的蹊跷:SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:SMgr和其它进程同样采用Binder通信,SMgr是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。SMgr提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向SMgr注册自己Binder就必需通过0这个引用号和SMgr的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对SMgr而言的,一个应用程序可能是个提供服务的Server,但对SMgr来说它仍然是个Client。

3.3Client 获得实名Binder的引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,SMgr象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。

3.4 匿名 Binder

并不是所有Binder都需要注册给SMgr广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向SMgr注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。

下图展示了参与Binder通信的所有角色,将在后面陆续提到。

图 1 Binder通信示例

4 Binder的表述

考察一次Binder通信的全过程会发现,Binder存在于系统以下几个部分中:

.应用程序进程:分别位于Server进程和Client进程中

.Binder驱动:分别管理为Server端的Binder实体和Client端的引用

.传输数据:由于Binder可以跨进程传递,需要在传输数据中予以表述

在系统不同部分,Binder实现的功能不同,表现形式也不一样。接下来逐一探讨Binder在各部分所扮演的角色和使用的数据结构。

4.1Binder 在应用程序中的表述

虽然Binder用到了面向对象的思想,但并不限制应用程序一定要使用面向对象的语言,无论是C语言还是C++语言都可以很容易的使用Binder来通信。例如尽管Android主要使用java/C++,象SMgr这么重要的进程就是用C语言实现的。不过面向对象的方式表述起来更方便,所以本文假设应用程序是用面向对象语言实现的。

Binder本质上只是一种底层通信方式,和具体服务没有关系。为了提供具体服务,Server必须提供一套接口函数以便Client通过远程访问使用各种服务。这时通常采用Proxy设计模式:将接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数,所不同的是Server端是真正的功能实现,而Client端是对这些函数远程调用请求的包装。如何将Binder和Proxy设计模式结合起来是应用程序实现面向对象Binder通信的根本问题。

4.1.1Binder 在Server端的表述– Binder实体

做为Proxy设计模式的基础,首先定义一个抽象接口类封装Server所有功能,其中包含一系列纯虚函数留待Server和Proxy各自实现。由于这些函数需要跨进程调用,须为其一一编号,从而Server可以根据收到的编号决定调用哪个函数。其次就要引入Binder了。Server端定义另一个Binder抽象类处理来自Client的Binder请求数据包,其中最重要的成员是虚函数onTransact()。该函数分析收到的数据包,调用相应的接口函数处理请求。

接下来采用继承方式以接口类和Binder抽象类为基类构建Binder在Server中的实体,实现基类里所有的虚函数,包括公共接口函数以及数据包处理函数:onTransact()。这个函数的输入是来自Client的binder_transaction_data结构的数据包。前面提到,该结构里有个成员code,包含这次请求的接口函数编号。onTransact()将case-by-case地解析code值,从数据包里取出函数参数,调用接口类中相应的,已经实现的公共接口函数。函数执行完毕,如果需要返回数据就再构建一个binder_transaction_data包将返回数据包填入其中。

那么各个Binder实体的onTransact()又是什么时候调用呢?这就需要驱动参与了。前面说过,Binder实体须要以Binde传输结构flat_binder_object形式发送给其它进程才能建立Binder通信,而Binder实体指针就存放在该结构的handle域中。驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节点,将Binder实体指针记录在该节点中。如果接下来有其它进程向该Binder发送数据,驱动会根据节点中记录的信息将Binder实体指针填入binder_transaction_data的target.ptr中返回给接收线程。接收线程从数据包中取出该指针,reinterpret_cast成Binder抽象类并调用onTransact()函数。由于这是个虚函数,不同的Binder实体中有各自的实现,从而可以调用到不同Binder实体提供的onTransact()。

4.1.2Binder 在Client端的表述– Binder引用

做为Proxy设计模式的一部分,Client端的Binder同样要继承Server提供的公共接口类并实现公共函数。但这不是真正的实现,而是对远程函数调用的包装:将函数参数打包,通过Binder向Server发送申请并等待返回值。为此Client端的Binder还要知道Binder实体的相关信息,即对Binder实体的引用。该引用或是由SMgr转发过来的,对实名Binder的引用或是由另一个进程直接发送过来的,对匿名Binder的引用。

由于继承了同样的公共接口类,Client Binder提供了与Server Binder一样的函数原型,使用户感觉不出Server是运行在本地还是远端。Client Binder中,公共接口函数的包装方式是:创建一个binder_transaction_data数据包,将其对应的编码填入code域,将调用该函数所需的参数填入data.buffer指向的缓存中,并指明数据包的目的地,那就是已经获得的对Binder实体的引用,填入数据包的target.handle中。注意这里和Server的区别:实际上target域是个联合体,包括ptr和handle两个成员,前者用于接收数据包的Server,指向 Binder实体对应的内存空间;后者用于作为请求方的Client,存放Binder实体的引用,告知驱动数据包将路由给哪个实体。数据包准备好后,通过驱动接口发送出去。经过BC_TRANSACTION/BC_REPLY回合完成函数的远程调用并得到返回值。

5 Binder内存映射和接收缓存区管理

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。我们注意到Binder驱动实现了mmap()系统调用,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd =open("/dev/binder", O_RDWR);

mmap(NULL,MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

6 Binder接收线程管理

Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,提供Binder实体,线程T1从Client进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。在这过程中,T2仿佛T1在进程S中的代理,代表T1执行远程任务,而给T1的感觉就是象穿越到S中执行一段代码又回到了C1。为了使这种穿越更加真实,驱动会将T1的一些属性赋给T2,特别是T1的优先级nice,这样T2会使用和T1类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来T2除了和T1优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。

对于Server进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求,即阻塞在accept()上。这个socket就象一只会生蛋的鸡,一旦收到来自Client的请求就会生一个蛋 – 创建新socket并从accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client实现交互。

可是对于Binder来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder。这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是Binder协议引入了专门命令或消息帮助用户管理线程池,包括:

· INDER_SET_MAX_THREADS

· BC_REGISTER_LOOP

· BC_ENTER_LOOP

· BC_EXIT_LOOP

· BR_SPAWN_LOOPER

首先要管理线程池就要知道池子有多大,应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder驱动还做了一点小小的优化。当进程P1的线程T1向进程P2发送请求时,驱动会先查看一下线程T1是否也正在处理来自P2某个线程请求但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有Binder实体并互相对发时请求时。假如驱动在进程P2中发现了这样的线程,比如说T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发送了请求尚未得到返回包,说明T2肯定(或将会)阻塞在读取返回包的状态。这时候可以让T2顺便做点事情,总比等在那里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。

7 数据包接收队列与(线程)等待队列管理

通常数据传输的接收端有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。当超市里的进货(数据包)太多,货物会堆积在仓库里;购物的人(线程)太多,会排队等待在收银台,道理是一样的。在驱动中,每个进程有一个全局的接收队列,也叫to-do队列,存放不是发往特定线程的数据包;相应地有一个全局等待队列,所有等待从全局接收队列里收数据的线程在该队列里排队。每个线程有自己私有的to-do队列,存放发送给该线程的数据包;相应的每个线程都有各自私有等待队列,专门用于本线程等待接收自己to-do队列里的数据。虽然名叫队列,其实线程私有等待队列中最多只有一个线程,即它自己。

由于发送时没有特别标记,驱动怎么判断哪些数据包该送入全局to-do队列,哪些数据包该送入特定线程的to-do队列呢?这里有两条规则。规则1:Client发给Server的请求数据包都提交到Server进程的全局to-do队列。不过有个特例,就是上节谈到的Binder对工作线程启动的优化。经过优化,来自T1的请求不是提交给P2的全局to-do队列,而是送入了T2的私有to-do队列。规则2:对同步请求的返回数据包(由BC_REPLY发送的包)都发送到发起请求的线程的私有to-do队列中。如上面的例子,如果进程P1的线程T1发给进程P2的线程T2的是同步请求,那么T2返回的数据包将送进T1的私有to-do队列而不会提交到P1的全局to-do队列。

数据包进入接收队列的潜规则也就决定了线程进入等待队列的潜规则,即一个线程只要不接收返回数据包则应该在全局等待队列中等待新任务,否则就应该在其私有等待队列中等待Server的返回数据。还是上面的例子,T1在向T2发送同步请求后就必须等待在它私有等待队列中,而不是在P1的全局等待队列中排队,否则将得不到T2的返回的数据包。

这些潜规则是驱动对Binder通信双方施加的限制条件,体现在应用程序上就是同步请求交互过程中的线程一致性:1) Client端,等待返回包的线程必须是发送请求的线程,而不能由一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包;2) Server端,发送对应返回数据包的线程必须是收到请求数据包的线程,否则返回的数据包将无法送交发送请求的线程。这是因为返回数据包的目的Binder不是用户指定的,而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程不是收到请求包的线程驱动将无从知晓返回包将送往何处。

接下来探讨一下Binder驱动是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端(client)在发出请求数据包后须要等待应答端(Server)的返回数据包,而异步交互的发送端发出请求数据包后交互即结束。对于这两种交互的请求数据包,驱动可以不管三七二十一,统统丢到接收端的to-do队列中一个个处理。但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:对于某个Binder实体,只要有一个异步交互没有处理完毕,例如正在被某个线程处理或还在任意一条to-do队列中排队,那么接下来发给该实体的异步交互包将不再投递到to-do队列中,而是阻塞在驱动为该实体开辟的异步交互接收队列(Binder节点的async_todo域)中,但这期间同步交互依旧不受限制直接进入to-do队列获得处理。一直到该异步交互处理完毕下一个异步交互方可以脱离异步交互队列进入to-do队列中。之所以要这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完毕以免影响请求端的响应速度,而异步交互属于‘发射后不管’,稍微延时一点不会阻塞其它线程。所以用专门队列将过多的异步交互暂存起来,以免突发大量异步交互挤占Server端的处理能力或耗尽线程池里的线程,进而阻塞同步交互。

8 总结

Binder使用Client-Server通信方式,安全性好,简单高效,再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,成为Android进程间通信的中流砥柱。

第1章 认识Android的IPC跨进程通信机制

每个平台都有跨进程的IPC机制,让不同进程里的两个程序之间能够互相沟通,在Android世界中,Binder框架提供IPC接口IBinder作为沟通管道。由于IBinder接口里只含有一个transact()函数来执行双方的沟通动作,这让App开发者觉得很不方便,于是Android提供了Proxy-Stub机制来将IBinder接口加以包装起来,提供了比较好用的接口,也减少了App与IBinder接口间的相依性。为了把IBinder接口包装起来,Android提供了AIDL的方式自动帮开发者生成Proxy-Stub对象。

1.1 Java层IBinder接口定义

Java层和C++层都定义了IBinder接口,我们以Java层来认识IBinder,该接口定义在IBinder.java文件里。

//IBinder.java

publicinterface IBinder {

// ..........

public boolean transact(int code, Parcel data, Parcel reply, int flags)

throws RemoteException;

// ...........

}

Android框架还提供了Binder基类和BinderProxy类别来实现IBinder接口,必要时,我们自己也能够实现它。

1.2 Java层的Binder基类及BinderProxy类定义

Binder.java文件中定义了两个类别,分别是Binder基类和BinderProxy内部类,Binder基类的目的是支持跨进程调用Service,也就是让远程的Clinet跨进程调用某个Service。

public class Binder implements IBinder {

// ..........

private int mObject;

public Binder() {

init();

// ...........

}

public final boolean transact(int code,  Parcel data,  Parcel reply, int flages) 

throws RemoteException {

// ................

boolean r = onTransact(code, data, reply, flags);

return r;

}

private boolean execTransact(int code,  int dataObj,  int replyObj, int flags) {

Parcel data = Parcel.obtain(dataObj);

Parcel reply = Parcel.obtain(replyObj);

boolean res;

res = onTransact(code, data, reply, flags);

// ............

return res;

}

protected boolean onTransact(int code, Parcel data, Parcel reply,  int flags)  

throws RemoteException {

}

private native final void init();

}

}

主要函数有

transact() 用来实现IBinder的接口

execTransact() 与transact()相同,是用来让C/C++本地程序来调用的

onTransact() 让应用子类来覆写的,上述两个方法都是调用onTransact()函数来实现反向调用IoC的

init() 本地函数, 让JNI模块来实现这个函数,Binder()构造方法会调用他。

PS: IoC(控制反转)是,获得依赖对象的过程被反转了。

图1 -1 Binder框架基类及其他本地模块

// .........

final class BinderProxy implements IBinder {

// ..........

BinderProxy() {

// .........

}

public nativeboolean transact(int code, Parceldata, Parcel reply, int flags) throws RemoteException;

private intmObject;

}

当我们看到XXXProxy类别时,就自然要联想到他是摆在Client进程里,担任Service端的分身(Proxy),架构图如下:

图1-2 BinderProxy类及其他本地模块

第2章 Activity 与Service的IPC 通信机制

这里,我们以启动一个服务,并绑定该服务为例,来初步认识IPC的过程。

2.1 startService做了什么

看如下代码片段

// myActivity.java

public class myActivity extends Activity implements OnClickListener {

........

public voidonCreate(Bundle icicle) {

........

startService(newIntent("xxx.xxx.xxx.REMOTE_SERVICE"));

........

}

}}

// myService.java

public class myService extends Service {

private IBindermb = null;

...........

@Override

public void onStart() {

mb = newmyBinder();

}

@Override

public IBinderonBind(Intent intent) {

return mb;

}

}

当myActivity調用startService()时,框架会为我们調用Service.onStart()函数,执行

mb = new myBinder(),进而调用父类Binder的构造函数,前面我们已经知道,Binder构造函数中会调用本地init()方法,这在C++层创建一个JavaBBinderHolder对象,并将该对象的引用赋值给Java层的myBinder对象的int mObject成员。如下代码片段所示:

//android_util_Binder.cpp

static void android_os_Binder_init(JNIEnv* env, jobject clazz)

{

JavaBBinderHolder* jbh = new JavaBBinderHolder(env, clazz);

if (jbh == NULL) {

jniThrowException(env,"java/lang/OutOfMemoryError", NULL);

return;

}

LOGV("Java Binder %p:acquiring first ref on holder %p", clazz, jbh);

jbh->incStrong(clazz);

env->SetIntField(clazz, gBinderOffsets.mObject, (int)jbh);

}

// gBinderOffsets对象

const char* const kBinderPathName = "android/os/Binder";

static int int_register_android_os_Binder(JNIEnv* env)

{

jclass clazz;

clazz = env->FindClass(kBinderPathName);

LOG_FATAL_IF(clazz == NULL,"Unable to find class android.os.Binder");

gBinderOffsets.mClass =(jclass) env->NewGlobalRef(clazz);

gBinderOffsets.mExecTransact =env->GetMethodID(clazz, "execTransact","(IIII)Z");

assert(gBinderOffsets.mExecTransact);

gBinderOffsets.mObject =env->GetFieldID(clazz, "mObject","I");

assert(gBinderOffsets.mObject);

returnAndroidRuntime::registerNativeMethods(

env, kBinderPathName,

gBinderMethods,NELEM(gBinderMethods));

}

如下图:

目前,已经执行完了startService()的动作,接下来bindService()去绑定想要的Service,如果找到这个Service,并且他没有被任何Client绑定的话,就会调用myService.onBind()函数,return mb,onBind()方法将mb传回给AMS,这个过程大致如下,bindService开始,AMS 调用requestServiceBindingLocked()来scheduleBindService(), 通过Handler的方式,最终调到下面的方法中,我们看到黑色加粗的部分,

private final void handleBindService(BindServiceDatadata) {

Service s = mServices.get(data.token);//这里先不做解释

if (s != null) {

try {

data.intent.setExtrasClassLoader(s.getClassLoader());

try {

if(!data.rebind) {

IBinder binder = s.onBind(data.intent);//service.onBind()返回的对象

ActivityManagerNative.getDefault().publishService(

data.token, data.intent, binder);

} else {

s.onRebind(data.intent);

ActivityManagerNative.getDefault().serviceDoneExecuting(

data.token, 0, 0, 0);

}

ensureJitEnabled();

} catch(RemoteException ex) {

}

} catch (Exception e){

if(!mInstrumentation.onException(s, e)) {

throw newRuntimeException(

"Unable to bind to service " + s

+ " with " + data.intent + ":" + e.toString(), e);

}

}

}

}

在publishService中,递归调用service上的所有ServiceConnection实现的public void onServiceConnected(ComponentName name,IBinder service),这里是AMS将myBinder传给了myActivity的 onServiceConnected,这就完成了跨进程的服务的绑定。一旦绑定成功,myActivity就可以和myService进行IPC跨进程通信了。

PS:有篇文章说AMS在得到myBinder(即IBinder接口)对象后时,可以通过C++层映射的JavaBBinderHoder对象get()出一个JavaBBinder对象,如下

sp<JavaBBinder> get(JNIEnv* env)

{

AutoMutex _l(mLock);

sp<JavaBBinder> b = mBinder.promote();

if (b == NULL) {

b = new JavaBBinder(env, mObject);//jobject mObject是Binder对象的指针

mBinder = b;

LOGV("CreatingJavaBinder %p (refs %p) for Object %p, weakCount=%d\n",

b.get(), b->getWeakRefs(),mObject, b->getWeakRefs()->getWeakCount());

}

return b;

}

之后,AMS在Client进程的Java层里创建一个BinderProxy对象来代表JavaBBinder的分身,也就是代表了myBinder的分身,最后将BinderProxy的IBinder接口回传给myActivity。如下图

绑定服务后,我们可以从myActivity调用BinderProxy执行IBinder接口的transact()函数。如下图IPC调用路径

红色虚线表示Java层通过Binder驱动才能调到BBinder的IBinder接口。

2.2 IPC通信的主要对象关系

2.2.1startService的相关对象

在前面,我们提到了Activity和Service之间IPC通信的三个步骤:

Step1:startService来启动服务

Step2:bindService来绑定服务

Step3:Activity调用IBinder接口的transact()函数,通过底层Binder Driver驱动接口间接调用Binder基类的execTransact()函数,转而调用myBinder的onTransact()函数。

如下图所示,startService动作完成后的对象关系。

2.2.2bindService的相关对象

bindService的调用过程在前面已经描述过,这里主要是说JavaBBinder对象是如何创建出来的,当AMS取得myService.onBind()返回的myBinder对象后,执行ActivityManagerNative.getDefault().publishService(data.token,data.intent, binder);

public void publishService(IBinder token, Intent intent,  IBinder service) throws RemoteException {

Parcel data =Parcel.obtain();

Parcel reply =Parcel.obtain();

data.writeInterfaceToken(IActivityManager.descriptor);

data.writeStrongBinder(token);

intent.writeToParcel(data,0);

data.writeStrongBinder(service);

mRemote.transact(PUBLISH_SERVICE_TRANSACTION, data, reply, 0);

reply.readException();

data.recycle();

reply.recycle();

}

接着调用JNI本地的android_os_Parcel_writeStrongBinder()函数,如下代码片段:

static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jobject clazz, jobject object)

{

    Parcel* parcel = parcelForJavaObject(env,clazz);

    if(parcel != NULL) {

        const status_t err =parcel->writeStrongBinder(ibinderForJavaObject(env, object));

        if (err != NO_ERROR) {

            jniThrowException(env,"java/lang/OutOfMemoryError", NULL);

        }

    }

}

 

sp<IBinder> ibinderForJavaObject(JNIEnv* env,jobject obj)

{

if (obj == NULL) return NULL;

if(env->IsInstanceOf(obj, gBinderOffsets.mClass)) {//obj是否是myBinder类别的对象

        JavaBBinderHolder* jbh =(JavaBBinderHolder*)

            env->GetIntField(obj,gBinderOffsets.mObject);//取得myBinder的mObject成员

//这里通过JavaBBinderHolder.get(){b = new JavaBBinder(env, mObject);}就创建了JavaBBinder对象。

        return jbh != NULL ? jbh->get(env) :NULL;

    }

if (env->IsInstanceOf(obj,gBinderProxyOffsets.mClass)) {

return (IBinder*)

env->GetIntField(obj, gBinderProxyOffsets.mObject);

}

LOGW("ibinderForJavaObject: %p is not a Binder object", obj);

return NULL;

}

//JavaBBinder类

class JavaBBinder : public BBinder

{

public:

JavaBBinder(JNIEnv* env,jobject object)

        : mVM(jnienv_to_javavm(env)),mObject(env->NewGlobalRef(object))

{

LOGV("CreatingJavaBBinder %p\n", this);

android_atomic_inc(&gNumLocalRefs);

incRefsCreated(env);

}

执行指令mObject(env->NewGlobalRef(object))将Java层的myBinder对象指针(即object)存入到JavaBBinder新对象的mObject属性里,如下图所示:

接着,onTransact ()

case PUBLISH_SERVICE_TRANSACTION:

data.enforceInterface(IActivityManager.descriptor);

IBinder token =data.readStrongBinder();

Intent intent =Intent.CREATOR.createFromParcel(data);

IBinder service = data.readStrongBinder();

publishService(token, intent, service);

reply.writeNoException();

return true;

//Parcel的JNI本地调用

static jobject android_os_Parcel_readStrongBinder(JNIEnv*env, jobject clazz)

{

Parcel* parcel = parcelForJavaObject(env,clazz); //判断是否设置了Parcel对象的mObject成员变量

if (parcel != NULL) {//实际上就是C/C++层的Parcel对象

return javaObjectForIBinder(env,parcel->readStrongBinder());

}

return NULL;

}

//Parcel对象构造器函数会调用native init()

static void android_os_Parcel_init(JNIEnv* env, jobject clazz, jintparcelInt)

{

Parcel* parcel =(Parcel*)parcelInt;

int own = 0;

if (!parcel) {

//LOGI("Initializingobj %p: creating new Parcel\n", clazz);

own = 1;

parcel = new Parcel;

} else {

//LOGI("Initializingobj %p: given existing Parcel %p\n", clazz, parcel);

}

if (parcel == NULL) {

jniThrowException(env,"java/lang/OutOfMemoryError", NULL);

return;

}

//LOGI("Initializing obj%p from C++ Parcel %p, own=%d\n", clazz, parcel, own);

env->SetIntField(clazz,gParcelOffsets.mOwnObject, own);

env->SetIntField(clazz,gParcelOffsets.mObject, (int)parcel);

}

在前面我们提到parcel->writeStrongBinder(ibinderForJavaObject(env,object));就已经把JavaBBinder对象存储于parcel对象的flat_binder_object结构中了,通过data.readStrongBinder()从flat_binder_object读取出JavaBBinder指针,然后创建一个BpBinder对象,成为JavaBBinder在Activity端的C/C++层的分身(Proxy),接着,调用JNI本地的javaObjectForIBinder()在Java层创建一个BinderProxy对象,成为BpBinder在Java层的分身。然后将BpBinder对象的引用存到BinderProxy对象的mObject成员中。

object = env->NewObject(gBinderProxyOffsets.mClass,gBinderProxyOffsets.mConstructor);

if (object != NULL) {

LOGV("objectForBinder%p: created new %p!\n", val.get(), object);

// The proxy holds a referenceto the native object.

 env->SetIntField(object,gBinderProxyOffsets.mObject, (int)val.get());

如下图所示:

之后将BinderProxy对象传回给Activity端,即

public void onServiceConnected(ComponentName className, IBinder ibinder){

mb = ibinder;

}

2.2.3执行mb.transact( )函数

Activity执行mb.transact()函数,实际上是BinderProxy的接口函数transact(),而tansact()转而调用JNI本地模块的transact()函数,如下图:

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobjectobj,

jint code, jobject dataObj,

jobject replyObj, jint flags)

{

if (dataObj == NULL) {

jniThrowException(env,"java/lang/NullPointerException", NULL);

return JNI_FALSE;

}

Parcel* data =parcelForJavaObject(env, dataObj);

if (data == NULL) {

return JNI_FALSE;

}

Parcel* reply =parcelForJavaObject(env, replyObj);

if (reply == NULL &&replyObj != NULL) {

return JNI_FALSE;

}

IBinder* target = (IBinder*)

env->GetIntField(obj,gBinderProxyOffsets.mObject);

if (target == NULL) {

jniThrowException(env,"java/lang/IllegalStateException", "Binder has beenfinalized!");

return JNI_FALSE;

}

LOGV("Java code callingtransact on %p in Java object %p with code %d\n",

target, obj, code);

// Only log the binder callduration for things on the Java-level main thread.

// But if we don't

const bool time_binder_calls =should_time_binder_calls();

int64_t start_millis;

if (time_binder_calls) {

start_millis =uptimeMillis();

}

//printf("Transact fromJava code to %p sending: ", target); data->print();

 status_t err = target->transact(code,*data, reply, flags);

//if (reply)printf("Transact from Java code to %p received: ", target);reply->print();

if (time_binder_calls) {

conditionally_log_binder_call(start_millis, target, code);

}

if (err == NO_ERROR) {

return JNI_TRUE;

} else if (err ==UNKNOWN_TRANSACTION) {

return JNI_FALSE;

}

signalExceptionForError(env,obj, err);

return JNI_FALSE;

}

BpBinder.transact()又跨进程调用到JavaBBinder的transact()方法,如下图所示

红色虚线表示并非直接调用,而是底层Binder驱动的事情,如下图所示

以上,就是大致的IPC通信过程。