Binder原理解析精炼
1 注册服务
-
Server进程向binder驱动向Binder驱动发起服务注册请求
向Binder驱动申请创建一个XXXService的Binder的实体,Binder驱动为这个XXXService创建位于内核中的Binder实体节点以及Binder的引用
-
Binder驱动将注册请求转发给ServiceManager进程
Binder驱动将名字和新建的引用打包传递给SM(实体没有传给SM),通知SM注册一个名叫XXX的Service
-
ServiceManager进程添加该server进程:从Binder驱动里取出handle和name,并构造结构体插入到链表。
此时,servicemanager进程拥有了Server进程的信息
2 获取服务
-
client向Binder驱动发起获取服务的请求,传递要获取服务的名称
-
Binder驱动将该请求转发给ServiceManager进程
-
ServiceManager查找到client需要的server对应的服务信息:从Binder驱动里取出name,并找到链表里相同的name,找到后取出handle,最后写入到Binder驱动
-
通过Binder驱动将上述服务信息返回给client进程
client进程与server进程已经建立联系
此时,如果有Client向SM发送申请服务XXXService的请求,那么SM就可以在查找表中找到该Service的Binder引用,并把Binder引用(XXXBpBinder)返回给Client。
3 使用服务
-
client端调用getService通过handler构造client端的BpBinder(native层),与此对应的是Java层的BinderProxy;
-
将ServiceManager的handler与请求服务名写入binder驱动,binder驱动通过SM拿到对应server的handler,从而找到Server进程,进而调用BBinder(Native层),与此对应的是Java层的Binder;
对应server的handler就是该server的Binder引用
-
将拿到的server的handler转换为BpBinder,然后转为java层的BinderProxy
-
client接口调用:调用BinderProxy.transact(),转为native层的BpBinder.transact(),然后转为handler,将数据写入binder驱动
-
Server端读取Binder驱动数据,找到BBinder对象,然后转为Binder对象,调用Binder.onTransact()完成对应接口调用,并通过Binder驱动返回处理结果给客户端的服务器代理对象
-
客户端收到服务端的返回结果
4.完整流程
5 常见问题
1 Binder的数据结构
Binder在传输数据中的表述:flat_binder_object
Binder对象类型
Binder实体在驱动中的表述:binder_node
Binder引用在驱动中的表述:binder_ref
Binder 进程、线程结构:binder_proc和binder_thread
Binder收发数据包结构:binder_transaction_data
Binder写操作命令字: BC_XXX
Binder读操作命令字: BR_XXX
2 一次拷贝
将内核缓存区和接收进程用户空间地址映射到一个接收缓存区
-
首先 Binder 驱动在内核空间创建一个数据接收缓存区
-
在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系
-
发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便实现了一次拷贝。
3 既然使用内存映射技术可以减少数据拷贝次数,为什么发送方进程不做内存映射?
发送方进程也做内存映射确实可以实现数据 0 拷贝传输,这就属于是以共享内存的方式进行 IPC 了。但是从以下两个角度来看,还是使用 Binder 的方式比较适合 Android
- 从性能角度来看: Binder 的数据只要拷贝一次,性能仅次于共享内存,且 RPC 调用的数据通常较小,因此拷贝操作对性能的影响并不大。
- 从稳定性和复杂性角度来看:Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题
4 oneway 机制
- 非 oneway 的 aidl 接口,调用方线程会被阻塞,直到被调用方进程返回为止
- oneway 的 aidl 接口,不会阻塞调用方线程,而是立即返回。在Android Framework 中 AMS 启动 Activity 时就是使用的 oneway 的方式,不会因为应用进程而阻塞了AMS进程的线程
- oneway 的还有一个特点,它是串行化的,binder 驱动内部有一个队列,会将它一个一个发送给接收进程
那么问题来了,oneway 的接口立即返回,怎么拿到被调用方进程的处理结果呢?
首先,如果我们不需要关注返回结果的,建议使用 oneway,如果需要返回结果,也可以用 oneway,有两种方式:
- 注册监听,将 callback 也定义成 aidl 接口,然后通过注册方式向接收方进程传递,接收方进程拿到 callback,通过 binder 机制可以向发送方回调数据
- 直接在定义方法的时候,传入 aidl 定义的 callback 接口,同上机制
但是要注意,通过单独注册监听的方式,如果需要反注册的话,接受方进程维护的监听列表,要使用 RemoteCallbackList 来存储。因为发送方的 callback 和接收方拿到的 callback 不是同一个对象,而 RemoteCallbackList 内部通过以 callback 对应的 binder 作为 key (虽然不是同一个对象,但是 binder 是同一个),所以可以保证注册和反注册正常,同时 RemoteCallbackList 内部做了线程安全保障(注册和反注册都是synchronized
方法),不需要自己额外处理多线程问题
5 Binder 通信的数据大小限制
在进程创建的时候会为 Binder 创建一个1M -8k的缓冲区用于跨进程通信时的数据传输
Binder 调用中同步调用优先级大于 oneway(异步)的调用
为了充分满足同步调用的内存需要,所以将 oneway 调用的内存限制到申请内存上限的一半
如果超过1M-8k这个上限就就会抛出这个异常,而且这个缓存区时当前进程内的所有线程共享的,线程最大数量为16个,如同时间内总传输大小超过了1M,也会抛异常。
另外在Activity启动的场景比较特殊,因为Binder 的通信方式为两种,一种是异步通信,一种是同步通信,异步通信时数据缓冲区大小被设置为了原本的1半
6 为什么要用Binder
简单的回答:性能:相比传统的Socket更高效;安全:安全性高,支持通信双方进行身份验证
7 Binder为啥用Parcelable做序列化
Serializable使用简单,但是开销很大(大量I/O操作),Parcelable是Android中的序列化方式,使用起来麻烦,但是效率很高,是Android推荐的方式。Parcelable主要用在内存序列化上,如果要将对象序列化到存储设备或者通过网络传输也是可以的,但是会比较复杂,这两种情况建议使用Serializable。
序列化
序列化是指将一个对象转化为二进制或者是某种格式的字节流,将其转换为易于保存或网络传输的格式的过程,反序列化则是将这些字节重建为一个对象的过程。Serializable和Parcelable接口可以完成对象的序列化。
1)Serializable
Serializable是Java提供的序列化接口,使用时只需要实现Serializable接口并声明一个serialVersionUID(用于反序列化)
2)Parcelable
A. writeToParcel:将对象序列化为一个Parcel对象,将类的数据写入外部提供的Parcel中
B. describeContents:内容接口描述,默认返回0
C. 实例化静态内部对象CREATOR实现接口Parcelable.Creator,需创建一个数组(newArray(int size)) 供外部类反序列化本类数组使用;createFromParcel创建对象
ribeContents:内容接口描述,默认返回0
C. 实例化静态内部对象CREATOR实现接口Parcelable.Creator,需创建一个数组(newArray(int size)) 供外部类反序列化本类数组使用;createFromParcel创建对象
D. readFromParcel:从流里读取对象,写的顺序和读的顺序必须一致。