IPC(Inter-Process Communication),在日常开发中大多数场景下都用不到,而且我们在开发中就算用到了进程间通信,基本上都只是专注于通信双方接口的设计,对于底层实现的细节并不是很了解。稍微看下Android Framework层的源码,也会发现随处可见IPC的身影。所以,有必要稍稍了解下其底层实现的原理,有助于加深我们对进程间通信的认识。
AndroidIPC概述
Android采用Binder作为IPC机制,Binder由于性能较高,采用了内存映射机制(mmap方法),内存拷贝只需一次,传统的linux进程间通信(管道、消息队列、共享内存、
信号量、Socket)最少也需要两次。
内存映射的实现过程主要是通过Linux系统下的系统调用函数:mmap()
该函数的作用 = 创建虚拟内存区域 + 与共享对象建立映射关系
用户空间:进程间相互独立
内核空间:所有进程共享
所以一个用户想给另一个用户发送数据必须通过内核空间完成,一般是将用户空间的数据拷贝至内核空间,然后另一个用户再将数据从内核空间拷贝至用户空间
用户1->拷贝->内核->拷贝->用户
而内存映射只需要一次拷贝:
用户1->拷贝->内核->映射->用户2
暂且这么认为吧
Binder跨进程模型整体架构:
Client
客户端,服务的使用者
Server
服务端,服务的提供者
ServiceManager
相当于一个路由器,所有的Service都在ServiceManager处登记和查询,将字符串的Binder名字转化为Client中对该Binder的引用。
Binder驱动
一种虚拟设备驱动,连接client,server,ServiceManager的桥梁。
Binder驱动的作用:
- 传递进程间的数据,通过内存映射
- 实现线程控制,采用Binder驱动本身的线程池,并由Binder驱动自身进行管理。
Binder驱动持有每个Server进程在内核空间中的binder实体,并给client进程提供binder实体的引用。
说明
client和server以及ServiceManager之间的交互都要通过Binder。Client和Server属于应用层,ServiceManager和Binder属于Android基础架构。
Binder请求的线程管理:
- 注意server提供的服务方法并不是运行在主线程的。不过其本身的创建过程依旧在主线程。Server进程会创建很多线程来处理Binder请求。
- Binder模型的线程管理 采用Binder驱动的线程池,并由Binder驱动自身进行管理
- 一个进程的Binder线程数默认最大是16,超过的请求会被阻塞等待空闲的Binder线程。
具体应用流程
注册服务
server进程通过Binder驱动先在ServiceManager注册服务
代码实现
Server进程 创建 一个 Binder 对象
获取服务
这里有一个querylocal的操作,如果client和server处于同一个进程,则直接返回server服务,不用跨进程。
使用服务
使用BinderProxy的代理方法,将方法code,方法参数写入parcel对象中,调用transact方法将数据发送到Binder驱动,Service端收到数据,解包后进行处理,写入返回结果,Binder驱动根据代理对象沿原路将结果返回,并通知Client进程获取返回结果,通过代理对象接收结果(之前被挂起的线程被唤醒)。
client端发起请求之后线程会被挂起,返回结果后会被唤醒。
具体过程可以查看aidl生成的代码。
相关类和接口
IBinder(定义了进程间通信的接口规范),
Binder(实现了IBinder接口),
IInterface(自己定义的aidl接口生成的代码必须继承这个接口),
Stub(生成的代码的静态内部类,继承于Binder和自己定义的接口,注意asInterface方法是核心,这个方法用来判断到底应该直接返回Binder还是BinderProxy,onTransact用于解包和执行服务的方法),
Proxy(Stub的静态内部类,Client端使用,用于代理服务端的Binder实现类,使用代理方法时,写入所有参数后,最终会调用remote.transact方法进行提交,之后就等待返回结果,线程挂起)
in,out,inout
自定义参数类型时,需要指定数据流向,in代表从client到server,out代表从server到client,inout表示双向流动。
定向Tag为in时,server端会反序列化client传过来的参数,执行完方法后,不会将参数再次序列化传回client端。
定向Tag为out时,client端不会将参数序列化传给server,而是由server自己实例化一个初始状态的参数,方法执行完之后,server会将自己实例化的参数序列化之后传回client端,client端利用自己传入的参数接收server传回的序列化数据。这里,client端传入的参数只是起了一个接收器的作用。
定向tag为inout时,client会将参数序列化传给server,同时server执行完方法后,也会将参数序列化传给client,client端利用自己传入的参数接收server传回的序列化数据。所以这里的数据流动是双向的。
注意事项
当使用RemoteCallback方式交互时,需要注意callback之前需要调用RemoteCallbackList.beginBroadcast(否则获取不到Callback对象),callback结束后要调用RemoteCallbackList.finishBroadcast。client传给Server的callback对象实际上也是一个binder,所有服务端才能回调。
Binder会为每个参与通信的进程分配内存,跨进程传输的数据是存储在Binder分配的内存中的,每个进程只能访问自己的内存,内存不超过1M(非ServiceManager),所以跨进程数据量不能太大。