Binder小结
什么是Binder
- Binder是Android中一种跨进程通信方式,Binder也是一个虚拟设备,对于客户端来说Bidner是一个可以跨进程通信的一个类
为什么Android要使用Binder进程间通信?
- Android底层是Linux,但是Linux已有的跨进程通信方式都不能满足Android移动设备的需求,在Android中跨进程通信方式,要求是CS的一对多的架构、需要保证安全,并且效率高;在传统Linux跨进程通信方式中,Socket是CS架构,但是它不够安全、而且需要两次从用户空间和内核空间之间的拷贝;管道它比较安全,但是它只适合1对1,并且也需要两次拷贝;共享内存效率很高,1次拷贝都不需要,支持多对多,但是它不安全;还有其他像信号量、文件、消息等通信方式都无法完全满足Android移动设备的需求,所以Google工程师就开发了Binder作为Android中主要的通信方式,它属于CS架构,支持1对多,在安全性上通过uid、pid进行鉴权,效率上只需要1次拷贝就可以了
Binder实现原理
Binder模型主要包含这么几个角色:客户端Client、服务端Service、ServiceManager、Binder驱动;
-
Binder驱动是实现进程间通信的核心,它的核心原理在于使用了mmap内存映射技术,在内核空间中,将内核数据缓冲区的一块内存和内核中数据接收缓冲区进行了内存映射,然后数据接收缓冲区又跟用户空间中接收数据方所在进程有内存映射关系,所以当数据发送方所在进程发送数据时,会先将数据序列化后拷贝到内核数据缓冲区,由于内核数据缓冲区和数据接收缓冲区以及接收方用户进程内存映射,所以相当于直接将数据发送到了数据接收方所在进程;它之所以高效的原因就是因为使用了内存映射技术,减少了从内核空间拷贝到数据接收方所在的进程的过程
-
ServiceManager主要用于管理AMS\PMS等系统服务对应的Binder;当客户端需要使用系统服务的时,通过ServiceManager拿到对应系统服务的Binder代理类,比如ActivityManager\WIndowManager;这些代理类内部会持有系统服务器对应的Binder,通过跨进程方式实现方法的调用;
-
ServiceManager是在系统启动时init进程中启动的,同时启动的还有Zygote进程,Zygote进程又会创建SystemServer进程,SystemServer进程又会去启动AMS、PMS这些系统服务,然后这些系统服务又会以Binder形式注册到ServiceManager中,ServiceManager会在Binder驱动红黑树中添加相应的节点,保存相应的信息
-
ServiceManager在native层启动时,会打开Binder驱动,将自身作为服务注册到Binder句柄中(句柄号为0),然后开启binder循环接收客户端发过来的请求,处理系统服务器的注册、查找等
-
-
为了简化客户端和服务端之间使用Binder跨进程通信的过程,Android提供了AIDL(全称是Android Interface define language);
-
在aidl文件中定义服务端要提供的方法,编译时会使用aidl命令将我们定义的接口生成一个java接口类;这个接口类继承自IInterface接口;里面主要有两个内部类Stub和Proxy代理类;
-
其中Stub类是继承自Binder并实现了我们定义的接口;其中有个onTransact方法以及用户定义的方法,在onTransact这个方法中会根据传递进来的参数,决定调用服务端哪个方法,并将结果进行Pacel序列化后返回,这个Stub类需要服务端在onBind方法中实现并作为Binder返回给客户端
-
另一个是Proxy代理类,当客户端拿到服务端返回的Binder对象时,需要将改Binder对象转换成这个Proxy代理对象,这个代理类实现了用户定义的接口,并代理服务端的Binder对象进行远程调用,当客户端通过这个代理对象调用服务端某个方法时,会将要调用的方法名、方法参数序列化后,通过Binder的transact方法发送出去
-
接着底层会通过BpBinder将数据转发到内核层的Bidner驱动,再通过内存映射直接发送到服务端所在进程,接着BbBinder接收到数据后会调用服务端Stub类里的onTransact方法,服务端这边再根据参数调用相应的方法并返回结果
-
Bundle传递数据为什么需要序列化?
因为Bundle传递数据,大多数是用于跨进程通信,进程之间内存空间是相互隔离的,无法直接访问,只有通过序列化之后,通过从用户空间拷贝到系统内核,再从系统内核拷贝到用户空间,才可以在另一个进程中通过反序列化后接收到
Binder线程池
在进程创建的时候,ProcessState里会为当前进程打开Binder驱动,通过mmap创建Binder数据接收缓存区,以及创建Binder线程池并开启主线程,默认最大是15个,Binder线程池中线程分为3类:主线程、普通线程、将当前线程加入到线程池的线程;
-
App中有多少Binder线程,是固定的吗?
-
app启动时在创建进程时默认会创建一个Binder主线程在运行,如果App中定义个其他服务在独立进程中,每个进程都至少会启动一个Binder主线程,后面根据跨进程通信请求次数,Binder会自动调成线程个数
-
最大的Binder线程也不是固定的,在ProcessState类中定义的默认最大线程个数是15个,这15个(不包括主Binder线程和将当前线程加入到Binder线程池中的),这个最大线程个数是可以修改的,比如SystemServer中就将线程池线程最大个数改成了31个
-
用户空间和内核空间区别?
用户空间是指app代码运行所在空间,内核空间是指系统代码、驱动、内核等运行所在的空间,之所以要划分用户空间和内核空间,是为了让app的代码和系统代码互相隔离开来,如果说没有划分,那么app代码和系统代码就都在一个进程里了,当app崩溃了,系统也会跟着奔溃,而且不同app进程之间也没办法公用系统代码,划分了之后,系统代码在内核空间,可以跟所有app公用,并且app崩溃不会导致内核崩溃,保证系统的安全
什么是物理地址和虚拟地址?
物理地址就是内存条的真实地址,虚拟地址是MMU内存管理单元出来之后才有的,虚拟地址是给cpu用的,cpu不能通过虚拟地址直接访问内存,需要通过MMU转换后才能访问到真实的物理内存;
最早期的计算机cpu是可以直接访问内存真实物理地址的,当时的软件都很小,可能只有几kb大小,可以将软件直接加载进内存运行,但是后面软件越做越大,如果直接全部代码加载到内存就会导致内存很快不够用,为了解决这个问题就引入了MMU对物理内存进行管理,CPU不能直接访问物理内存,需要访问物理内存时需要经过MMU将虚拟地址转换成物理地址才行;执行的代码也只加载当前活跃的代码到内存中,执行完后再从磁盘告诉缓冲中读取,
Binder最大传输数据是多少?为什么?
-
对于普通app来说是1M-8k,因为在创建进程的时候,ProcessState会在app用户空间和内核之间会通过mmap开辟一块空间进行内存映射,这块空间的大小就是1M-8k,进程之间传递数据都是通过这块内存来完成的,所以普通app最大限制是1M-8k
-
但是我们也可以在jni开发中手动打开binder驱动,然后调用mmap方法定义一个超过1M-8k大小的内存映射空间用于进程之间传递数据,但是也不能是无限大的,因为在mmap方法中限制最大只能开辟4M的内存映射空间大小;而且不建议这么做,因为Binder设计的最初目的并不是为了跨进程传递大数据,而只是用于通信用的,如果要传递大数据,完全可以通过文件、Socket、共享内存等其他方式传递;如果强制重新开启Binder可能会对已有Binder驱动造成影响,从而影响app正常与系统服务的交互
-
之所以是1M-8K而不是1M或者其他,是因为MMU内存管理单元从磁盘加载活跃代码时,为了避免频繁的IO,所以规定每次从磁盘至少读取1页的数据,1页就是4k数据大小,所以大小指定是4k的整数倍
-
实际使用过程中往往可传递的数据小于1M-8k,这是因为Binder线程中可能有多个线程(最大15个)正在跨进程通信,多个线程之间是共用这1M-8k大小的;而且在跨进程过程中,往往除了用户要传递的数据,还需要携带包括进程信息、目标进程信息、要调用的方法等等信息在里面,所以实际可能的空间往往更小
简单讲讲mmap原理?Binder如何做到1次拷贝?
Binder会在内核空间开辟两块内存,一块用于接收从发送方进程传递过来的数据,一块用于与接收方进程通过mmap进行内存映射,这几块内存空间之间都相互映射,当数据通过copy_from_user拷贝到内核缓冲区时,因为内存映射关系,接收方进程可以直接读取到这部分数据,从而省去了copy_to_user将数据从内核拷贝到接收方进程这一步,从而实现1次拷贝
内存中的1页是什么,你怎么理解的?
1页指的是硬盘数据加载到内存时,一次读取的数据数量是4k,这是为了避免频繁的进行io导致的性能问题
AIDL生成的java类结构?
生成的接口类包含了我们在aidl中定义的方法,继承自IInterface,里面有两个内部类分别是Proxy和Stub,他们都实现了我们定义的接口;
-
其中Stub类继承自Binder,内部有个onTransact方法,当跨进程远程调用服务端方法的时候,这个方法会被调用,在这个方法里会判断要请求的是哪个方法,然后调用服务端中这个Stub类的实现类中的方法;并将要返回的结果写入Parcel对象中
-
而Proxy代理类是用于代理Binder对象进行跨进程访问,当调用代理类的方法时,内部会先将要请求的参数写入Parcel对象中,然后调用Binder的transact方法进行远程调用并返回结果
-
当调用Binder的transact方法后,底层会通过BpBinder封装数据后转发远程调用请求,然后将数据拷贝到内核层,通过Binder驱动转发给服务端,服务端进程那边会通过BbBinder对数据进行解析,最终调用服务端Stub实现类里的onTransact方法
BindService启动Service与Binder服务实体的流程?
-
客户端调用bindService方法后,会跨进程调用AMS方法去查找要启动服务的信息,判断对应的服务所在进程是否已经启动,如果还没有则先通知Zygote启动进程
-
进程启动后会检查对应的Service是否已经创建,如果没有的话会通知ActivityThread先创建服务,创建完了之后会调用它的生命周期方法onCreate和onBind,在onBind方法中服务端会实现Stub类,这个Stub类继承自Binder
-
然后Service所在进程会将Binder对象返回给AMS,ANS则会回调客户端的ServiceConnection接口的onServiceConnected方法,并把服务端onBind方法返回的Binder对象返回给客户端
-
客户端拿到Binder对象后调用asInterface方法,这个方法里会根据是否跟服务端在一个进程中,来决定是返回服务端接口本身,还是返回支持跨进程通信的代理类Proxy
-
接着客户端就可以直接调用Service提供的方法了
Binder如何找到目标进程的?
binder服务会事先在Binder驱动的红黑树中注册结点,当跨进程通信时Binder驱动会先从红黑树中查找目标Binder所在结点,从结点中获取进程、线程信息,然后去唤醒目标进程、线程
Binder严格意义上拷贝几次?
拷贝两次,在从用户空间拷贝到内核空间的时候,调用了两次copy_from_user方法,一次用于拷贝传递的方法和参数等信息、另一次拷贝进程、线程等附加信息;之所以两次而不是一次,可能是考虑到如果第一次拷贝失败时,就可以直接return,而不用进行第二次拷贝了,节约了拷贝的时间
Binder中的红黑树,为什么会有两棵binder_ref红黑树
一个是用于通过句柄查找,一个是用于通过结点查找,提高查找效率
系统服务与bindService等启动的服务的区别
系统服务的注册、和查找是通过ServiceManager完成的,而bindService则主要是通过AMS来完成的
Binder线程、Binder主线程、Client请求线程的概念与区别
在进程创建的时候,process_state类中会创建Binder线程池,创建Binder线程池所在线程叫做Binder主线程,它会监听客户端发过来的Binder的请求,然后为会客户端分配Binder线程,Binder线程是通过线程池创建的;客户端请求线程就是客户端发起Binder请求所在线程文章来源:https://www.toymoban.com/news/detail-402376.html
为什么内核有内核缓存区还要有个数据接收缓存区?
因为内核缓冲区是一直就存在的,以往的跨进程通信就是先从发送方拷贝到内核缓存区,然后再拷贝到接收方,所以需要两次拷贝,Binder的出现创建了一块数据接收缓存区,通过mmap将接收方和系统内核缓存区连接起来,从而减少了一次拷贝文章来源地址https://www.toymoban.com/news/detail-402376.html
到了这里,关于Android Binder小结的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!