在Linux系统中,采用的是虚拟内存地址,分为内核空间和用户空间,运行在内核空间的进程可以直接进行通信,常为系统进程。而客户程序进程则运行在用户空间,它们之间的通信则需要通过内核空间进行。这样区分主要避免客户程序直接操作内核空间,影响系统的正常运行,对系统的操作交给执行在内核空间的操作系统的程序去执行,避免造成奔溃。
用户程序进程之间的通信,Linux系统支持一下几种。
管道:管道又分匿名管道和命名管道。一端写,一端读,是一种半双工的通信,单向数据流。管道效率较低,涉及四次拷贝:用户空间->内核空间->内存->内核空间->用户空间。且双向通信需要开启两条管道,对资源占用也是比较高。
匿名管道只能在有血缘关系的进程之间进行通信,如父子进程,兄弟进程。由于匿名管道是建立在内存的缓存区上,所以容量一般不太大,且传递的字节流格式需要双方约定。
命名管道则是在实际的文件系统上实现的通信机制,遵循先进先出原则。
信号:是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。接收进程接收到该信号后去做相应的处理逻辑。
消息队列:存放内核中的消息链表,处理进程不停读取消息内容,然后处理。
共享内存:直接读写同一块内核空间,最快IPC形式。依靠同步机制互斥。
套接字:套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。
Binder
机制是Android
跨进程通信的一种常用方式之一,例如ContentProvider
、MediaServer
等等底层大多数进程间通信都是基于Binder
机制。
在应用层之上想使用Binder机制进行跨进程通信一般采用AIDL
进行开发。
一、序列化
在开始介绍AIDL
之前,先介绍一下对象序列化与反序列化。对象要持久化到内存、本地磁盘或者在网络上传输,需要进行序列化。Android序列对象可以通过
1.Serializable 接口
Serializable
接口是Java所提供的,为对象提供标准的序列化和反序列化操作。通常一个对象实现Serializable
接口,该对象就具有被序列化和反序列化的能力,而且几乎所有工作有系统自动完成。Serializable
接口内serialVersionID
可指定也可以不指定,其作用是用来判断序列化前和反序列化的类版本是否发生变化。该变量如果值不一致,表示类中某些属性或者方法发生了更改,反序列化则出问题。(静态成员变量和transient关键字标记的成员不参与序列化过程)
2.Parcelable 接口
Parcelable
接口是Android所提供的,其实现相对来说比价复杂。实现该接口的类的对象就可以通过Intent
和Binder
进行传递。
3.两者的区别
Serializable
是Java提供的接口,使用简单,但序列化与反序列化需要大量的IO操作,所以开销比较大。Parcelable
是Android
提供的序列化方法,使用麻烦当效率高。在Android
开发中,将对象序列化到设备或者序列化后通过网络传输建议使用Serializable
接口,其他情况建议是用Parcelable
接口,尤其在内存的序列化上。例如Intent和Binder传输数据。
二、AIDL
在Java层,想利用Binder
机制进行跨进程的通信,那就得通过AIDL(Android 接口定义语言)了,AIDL是客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口,只有允许不同应用的客户端用 IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用 AIDL,如果是在单应用(单进程),建议使用Messager
。
1、AIDL支持的数据类型
Java
编程语言中的所有原语类型(如int
、long
、char
、boolean
等等);String
和CharSequence;
- 所有实现了
Parcelable
接口的对象; - AIDL接口;
List
,目前List只支持ArrayList
类型,持有元素必须是以上讲到类型;Map
,目前只支持HashMap
类型,持有元素必须是以上讲到类型;
自定义的Parcelable
对象和AIDL接口必须显示导入到AIDL文件中。
数据的走向
Parcelable
对象和AIDL接口在使用前必须标明数据的走向:
in
客户端流向服务端的数据out
服务端流向客户端的数据inout
服务端与客户端双向流向的数据
示例:
1 | void addUser(inout User user); |
2、服务端的实现
通过创建一个Demo
来实践AIDL。这里通过新建一个module
来表示一个新的应用进程。
2.1、定义数据对象
定义User
类,实现了Parcelable
接口,作为客户端和服务端传输的数据对象。
1 | public class User implements Parcelable { |
2.2、抽象服务端的服务能力
通过下面方法,建立一个UserManger.aidl
文件,表示服务端能为客户端提供什么样的服务。通过Android Studio的右键来新建一个AID。
下面代码通过建立UserManager.aidl
文件,为客户端提供addUser
和getUser
的能力。UserManager.aidl
可以理解为,服务端和客户端的共同约定,两者能进行怎么样的交互。
1 | package com.gitcode.server; |
在相同目录下创建User.aidl
,可以直接复制UserManager.aidl
,内容修改如下。
1 | package com.gitcode.server; |
定义UserManager.aidl
文件后,sync
后,系统默认会生成UserManager.java
文件。
UserManager.java的代码如下,为了减少篇幅,去掉了一些实现。
1 | public interface UserManager extends android.os.IInterface { |
从上文可知,UserManager
本身是一个接口,并继承IInterface
接口。UserManager.java
声明了addUser
和getUser
,和在UserManager.aidl
的声明是一致的。同时声明两个整型TRANSACTION_addUser
和TRANSACTION_getUser
,用于在transact()
方法中标识调用服务端哪个方法。如果服务端和客户端在不同进程,方法调用会走transact()
方法,逻辑由Stub
和Proxy
内部类完成。
内部类Stub的一些概念和方法含义:
DESCRIPTOR
Binder
的唯一标识,一般用当前的类名全名标识。
asInterface(IBinder obj)
将服务端的Binder对象转换成客户端的AIDL接口类型的对象,如果客户端和服务端同一进程,直接返回Stub对象本身,不在同一进程,则返回由系统封装的Stub.proxy
对象。
asBinder
返回当前Binder
对象
onTransact(int code, Parcel data, Parcel reply, int flags)
运行在服务端Binder
线程池,当客户端跨进程发起请求后,系统封装后交由此方法来处理。code
表示调用服务端什么方法,上文声明的整型。data
表示客户端传递过来的数据,reply
为服务端对客户端的回复。
内部代理类 Poxy
,客户端持有服务端的一个代理引用,通过该引用调用相关服务。
在服务端进程,通常提供多种服务,而每种服务而通过Service来体现,下面定义UserService
,继承至Service
。
1 | public class UserService extends Service { |
在AndroidManifest.xml文件声明Service,以两个组件形成单独的app来体现两个进程,通过AIDL进行数据交互。在客户端通过bindService()
来启动该服务。
1 | <service android:name="com.gitcode.server.UserService" |
3、客户端的实现
客户端主要是通过共同的约定(UserManger.aidl
)向服务端进行请求,服务端响应客户端的请求。为了提高效率和减少出错,通过拷贝来实现客户端的AIDL文件。将服务端的aidl
整个文件拷贝到客户端的main目录下,不做任何修改。
在客户端建立与服务端User类同包的目录,并将User类拷贝过来,不做任何修改。
在Activity中绑定服务端的Service,绑定成功后进行数据交互。
1 | public class MainActivity extends AppCompatActivity { |
运行效果:
客户端:
服务端:
4、小结
客户端调用服务的方法,被调用的方法运行在服务端的的Binder线程池,同时客户端会被挂起,如果服务端方法执行耗时操作,就会导致客户端ANR,所以不要在客户端主线程访问远程服务方法。同时服务端不应该自己新建新建线程运行服务方法,因为方法会交由线程池处理,同时对数据也要做好并发访问处理。
AIDL可以说为应用层开发提供了封装,不用过多的了解Binder的机制,通过生成的UserManager.java,初步可以了解Binder的IPC机制。使用AIDL在进程之间进行数据通信,更注重的是细节和业务的实现。
三、Binder
Binder进制是Android系统提供的一种IPC机制。由于Android是基于Linux内核,之所有不使用原有的 IPC机制,是因为使用Binder机制,能从性能、稳定性、安全性带来更好的效果。例如,Socket是一套通用的接口,传输速率低下,适合网络传输这种情况,而管道和消息队列需要数据的两次拷贝,共享内容难以管控等。而Binder对数据只需要一次拷贝,使用C/S架构,职责明确,容易维护和使用。
通过下图可以了解到,Binder机制通过内存映射实现跨进程通信,Binder在IPC机制只是作为一个数据的载体,当进程A向虚拟内存空间中写入数据,数据会被实时反馈到进程B的虚拟内存空间。整个发送数据的过程,只从用户空间拷贝一次到虚拟内存空间。
在Binder机制中,主要涉及到Client
、Server
、ServiceManger
,三者通过Binder进行跨进程通信,支撑着Android这个大网络。它们的关系如下图。
服务端
服务端,表示提供服务能力的一方,服务端通过向ServiceManger
中注册一些Service
,对外告知其可提供的服务。例如上文AIDL中,会注册UserService
,并为客户端提供添加User和获取User的操作。
客户端
对服务端提供的服务进行业务逻辑操作。通过服务端提供的Service
的名称在ServiceManger
查询对应的Service
。
ServiceManager
ServiceManger
集中管理所有Service
,和施加权限。为服务端提供注册Service
,为客户端提供查找Service
能力,类似网络编程中DNS
的作用。
图表示三者的C/S架构,例如Client查询向ServiceManger
查询Service
时,Client
就是客户端,而ServiceManger
就是服务端。而虚线则表示两者之间通过Binder进行进程间的通信,因此通过了解一条虚线的流程,就可以知道Binder的机制。
1、Service的注册过程
我们常通过getSystemService()
函数来获取系统的相关服务,例如ActivityManagerService
,那么这些Service从哪里来呢?就是有些服务进程向ServiceManager
注册自己的Service
,表示自己能对客户端提供什么样的服务。通过了解Service
的注册过程,来学习Binder
机制的工作原理。
BpServcieXXX
和BnServcieXXX
是客户端与服务端进程业务层辑实现的封装。BnServiceManger
派生自IServiceManager
和BBinder
类,通过调用BBinder
对象的transcat
函数的参与通信。是一个虚类,具体的功能需要子类去实现,存在服务端。BpServiceManager
派生自BpInterfacle
和``BpRefBase接口,其中
BpRefBase的
mRemote函数返回的就是
BpBinder`对象,存在客户端。
BBinder
和BpBinder
都是IBinder
的子类。两者相互对应,客户端通过BpBinder
对象与服务端进行通信,而客户端通过BBinder
与客户端进行通信。
通过IServiceManger
与IBinder
架构将业务逻辑与IPC进行分离,下层Binder机制完全可以切换成Linux中的其他通信机制。
1.1 ProcessState
每个进程通过单例模式创建了唯一的ProcessState
对象,在其构造器中,通过open_driver()
方法打开了/dev/hwbinder
设备,相当于Server进程打开了与内核的Binder
驱动交互的通道,并设置最大支持线程数为15。binder
设备是Android
在内核中为完成进程间通信而专门设置的一个虚拟设备。
1.2 注册相关Service
BpServiceManger
对象实现对IServiceManger
的业务函数,又有BpBinder
作为通信代表,下面分析一下注册的过程。
将字符串名字和Service对象作为参数传到BpServiceManger
对象的addService()
函数,该方法将参数数据打包后传递给BpBidner
的transact()
函数。业务层的逻辑到此就结束,主要作用是将请求信息打包交给通信层去处理。
在BpBinder
的transact()
函数调用了IPCThreadState
对象的transact()
函数,所以说BpBinder
本身没有参与Binder设备的交互。每个线程都有一个IPCThreadState
对象,其拥有一个mOut
、mIn
的缓冲区,mOut
用来存储转发Binder设备的数据,而mIn
用来接收Binder
设备的数据。通过ioctl
方式与Binder
设备进行交互。
1.3 小结
通过上文Service
的注册过程,分析了Binder的机制。Binder只是通信机制,业务可以基于Binder机制,也可以基于其他IPC方式的机制,也就是上文为啥有BpServiceManger
和BpBinder
。Binder之所以复杂,是Android通过层层的封装,巧妙的将业务与通信融合在一起。主要还是设计理想很牛逼。
2、ServiceManger
通过1小节的分析,是否应该也有一个类继承自BnServiceManger
来处理远方请求呢?
很可惜的是在服务端并没有BnServiceManger
子类来响应远程客户端的请求,而是交给了ServiceManger
来处理。
2.1 成为Service管理中心
ServiceManger
通过binder_open
函数打开binder
设备,并映射内存。通过handler
等于0标识自己,让自己成为管理中心,所有service
向ServiceManger
注册时,都是通过handle
标识为的0
的BpBinder
找到ServiceManger
对应的BBinder
,ServiceManager
会保存要注册的Service
的相关信息,方便Client查找。并不是所有的Service
都可以在ServiceManger
注册,如果Server
进程的权限不够root
或system
,那么需要在allowed
添加相应的项。
2.2 ServiceManger
集中管理带来的好处
- 统一管理,施加管控权
- 通知字符串名称查找Service
- Server进程生命无常,通过
ServiceManger
,Client可以实时知道Server进程的最行动态。
3、Client
Client想要使用Server进程提供的Service,又该进行哪些步骤呢?
3.1 查询ServiceManger
Client想要得到某个Service的信息,就得与ServiceManager
打交道,通过调用getService()
方法来获取对应Service
信息。Client通过服务名称向ServiceManger
查询对应的Service
。如果Service
未注册,则循环等待直到该Service
注册;如果已注册,则会对应封装了一个能与远程Service
通信的BpBinder
的BpXXXService
,通过该Service
,Client客户调用相关业务逻辑函数。
3.2 请求信息的处理
Client调用的业务函数,莫非就是将请求参数打包发送给Binder驱动,BpBinder
通过handler的值找到对应端的Service
来处理。
在1.4小节中,说到IPCThreadState
对象,在其executeCommand
函数中,通过调用实现了BnServiceXXX
的对象onTransact
函数,直接定位到业务层。这就是在AIDL中,为什么在onTransact
()函数中处理响应数据。
四、总结
通过对Binder机制的学习,了解Android是如何通过层层封装将Binder机制集成要应用程序,对Binder机制有一个较深入的理解。可以通过第Java层AIDL的使用,加深对Binder机制的理解。
个人水平有限,有误请帮忙勘正,谢谢大佬。喜欢就帮忙点个赞呗。