android Binder机制

本文转自一位大佬的博客,原文地址

首先我们可以带着疑问来理解binder机制,也就是binder机制在Android中有哪些地方运用了?
看你是否能回答如下问题
1.为什么activity之间传递对象需要序列化?
2.activity的启动流程是什么样?
3.四大组件底层通信机制是什么样的?
4.aidl内部实现原理是什么?
5.插件化编程技术应该如何学起等等
上边的问题背后都跟Binder有很大的关系,如果真的想理解上边的问题,那么一定先要理解Binder机制。

我们知道android应用程序中是由activity,service,broadcastReceiver和contentProvider四大组件中的一个或多个组成。由时这些组件运行在同一个进程中,有时运行在不同的进程中。而这些组件间的通信就需要依赖Binder机制,不仅如此,android应用层对系统提供的各种服务如:activityManagerService,PackageManagerService等都是基于Binder机制来实现的,Binder机制在android中的位置非常重要,毫不夸张的说理解Binder机制是迈向高级android开发工程师的第一步。

二.为什么是Binder?
android系统是基于Linux内核的,Linux已经提供了管道,消息队列,共享内存和Socket等IPC机制,那为什么android还要一枝独秀的提供Binder来实现进程间通信呢?主要是因为从性能,稳定性和安全性这几个方面的原因。
性能
首先说说性能方面的优势,Socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信,消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder只需要拷贝数据一次,性能上仅次于共享内存。
IPC方式 数据拷贝次数
共享内存 0
binder 1
Socket/管道/消息队列 2

稳定性
再说说稳定性,binder基于c/s机构,客户端有什么需求就丢给服务端去完成,架构清晰,职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制困难,难以使用。从稳定性的角度来讲,Binder机制是优于共享内存的。
安全性
另外一个方面就是安全性,android作为一个开放性平台,市场上有海量的应用供用户安装,因为安全性对android来说至关重要,作为用户当然不希望我们下载的app偷偷的读取我的通信录,上传我的隐私数据,后台偷跑流量,消耗手机电量。传统的ipc没有任何的安全措施,完全依赖于上层协议来确保,首先传统的ipc接收方无法获得对方可靠的进程用户id/进程id,从而无法前别用户身份。android为每个安装好的app分配了自己的uid,故而进程的uid是鉴别身份的重要标志,传统的ipc只能由用户在数据包中填入了uid/pid,但这样不可靠,容易被恶意程序利用。可靠的身份标志应该只能由ipc机制内核中添加。其次传统的ipc访问接入点是开放的,只要知道这些接入点的程序都可以和对端建立链接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接,同时binder既机制实名binder,又支持匿名binder,安全性高。
所以用一张表格来总结,如下
优势 描述
性能 只需要一次数据拷贝
稳定性 基于c/s架构,职责明确,架构清晰,因此稳定性好
安全性 为每个app分配uid,进程的uid是鉴别进程身份的重要标志

三.Linux下传统的进程间通信原理
了解linux ipc相关的概念和原理有助于我们理解binder通信原理,因此,在介绍binder跨进程通信原理之前,我们先聊聊Linux系统下传统的进程间通信是如何实现的。
3.1 基本概念介绍
这里我们先从Linux中进程间通信涉及的一些基本概念开始介绍,然后慢慢展开,向大家说明传统的进程间通信的原理。

在这里插入图片描述

上图展示了跨进程通信涉及到的一些概念
1.进程隔离
2.进程空间划分:用户空间和内核空间
3.系统调用:用户态/内核态
进程隔离
简单的说就是操作系统中,进程和进程内存是不共享的。两个进程就是两个平行的世界,A进程没法直接访问B进程,这就是进程隔离的通俗解释。A进程和B进程要通信就得使用特殊的通信机制:跨进程通信。
进程空间划分:用户空间和内核空间
现在操作系统都是采用虚拟存储器,对于32位系统而言,它的寻址空间就是2的32次方,也就是4GB。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限。为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间和内核空间。针对Linux而言,将最高的1GB供内核使用,成为内核空间,较低的3GB字节供各进程使用,称为用户空间。
简单来说:内核空间是系统内核运行的空间,用户空间是用户程序运行的空间,为了保证安全性,它们之间是隔离的。

在这里插入图片描述

系统调用:用户态和内核态
虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免用户空间需要访问内核资源,比如文件操作,访问网址等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。
Linux使用两级保护机制:0级供系统内核使用,3级供用户程序使用
当一个任务执行系统调用而进入内核代码中执行时,称进程处于内核运行态,此时处理器处于特权级最高的内核(0级)代码执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当进程在执行自己的代码时,我们称为用户运行态,此时处理器在特权级最低的(3级)用户代码运行。
系统调用主要通过如下两个函数来实现:
copy_from_user() // 将数据从用户空间拷贝到内核空间
copy_to_user() // 将数据从内核空间拷贝到用户空间

3.2 Linux下传统的ipc通信原理
理解了上边的几个概念,我们再来看下传统的ipc方式中,进程间通信是如何实现的?
通常的做法是消息发送方将要发送的数据缓存到内存缓存中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用copy_from_user()函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区。同样的,接收方在接受数据的时候在自己的用户空间开辟一块内存缓存区,然后内核程序调用copy_to_user()函数将数据从内核缓存区拷贝到接收方的内存缓存区。这样数据发送方进程和数据接收方进程完成了一次数据传输,我们称完成了一次进程间通信。如下图:

在这里插入图片描述

这样传统的ipc通信机制有两个问题:
1.性能低下,一次数据传递需要经历:内存缓存区--------内核缓存区--------内存缓存区,需要两次数据拷贝
2.接受数据的缓存区由接受数据进程提供,但是接收进程并不知道需要开辟多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用api接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。

四.Binder跨进程通信原理
理解了Linux ipc的相关概念和通信原理,接下来我们正式介绍下Binder ipc的原理
4.1 动态内核可加载模块 && 内存映射
正如前面说的,跨进程通信需要内核空间做支持的。传统的ipc机制如管道,socket都是内核的一部分,因此通过内核支持来实现跨进程通信自然是没问题。但是Binder并不是linux系统内核的一部分,那怎么办呢?这就得益于linux的动态内核可加载模块的机制,模块是具有独立功能的程序,它可以被单独编译,但是不可独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,android系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来进行通信。
在android系统中,这个运行在内核空间,负责各个用户进程通过binder实现通信的内核模块就叫做 binder驱动。
那么在android系统中用户进程之间是如何通过动态内核可加载模块(binder驱动)来实现通信的呢?难道和前边说的传统的ipc机制一样?先将数据从发送方拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方内存缓存区,通过两次数据拷贝来实现吗?显然不是的,否则也不会有开篇说的性能上边的优化。
这就不得不知道linux的另外一个概念:内存映射。
binder ipc机制涉及到的内存映射是通过mmap()函数来实现的,mmap()是操作系统中内存映射的一种方法。内存映射简单的来讲就是将用户空间的一块内存区域映射到内核空间。映射建立后,用户对这块内存空间的修改直接反应到内核空间上,反之内核空间对这块区域的修改也会直接反应到用户空间。

内存映射能减少数据拷贝次数,实现用户空间和内核空间的有效互动。两个空间各自的修改能直接反应在映射区域,从而被对方空间及时感知。也正因为如此,内存映射能提供进程间通信的支持。

4.2 Binder IPC实现原理
Binder IPC正是基于内存映射来实现,但是mmap()通常是用在有物理介质的文件系统上。
比如进程的用户区域是不能直接跟物理设备打交道的,如果想把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘---->内核空间---->用户空间),通常在这种场景下mmap()函数就发生作用了,通过在物理介质和用户空间之间建立映射,减少数据拷贝次数,通过内存读写来取代IO读写,从而提高文件的读写效率。
而Binder并不存在物理介质,因此binder驱动使用并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据的缓存空间。

一次完整的Binder IPC通信过程通常是这样的:
1.首先Binder驱动在内核空间创建一个数据接收缓存区。
2.接着在内核空间创建一个内核缓存区,建立内核缓存区和内核数据接收缓存区建立映射关系,以及建立内核数据接收缓存区和接收进程用户空间地址的映射关系。
3.发送方进程通过系统调用copy_from_user()将数据复制到内核缓存区,由于内核缓存区跟内核数据接收缓存区以及接收方用户空间存在映射关系,因此也就相当于把数据发送给了接收方用户空间,这样便完成了一次进程间通信。如下图

在这里插入图片描述

五 Binder通信模型
介绍完binder ipc的通信原理,接下来我们看看实现层面是如何设计的。
一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方为客户端进程和服务端进程,由于进程隔离的存在,双方通信必然要借助binder来实现

5.1 Client/Server/ServiceManager/Binder驱动
前面我们介绍过Binder是基于c/s架构的。由一系列的组件组成,包括client,server,serviceManager,binder驱动。其中client,server,serviceManager是运行在用户空间,而binder驱动运行在内核空间。其中serverManager和binder驱动都是由系统提供,而client,server是由应用程序来实现。client,Service和ServiceManager都是通过系统调用open,mmap和ioctl来访问设备文件/dev/binder,从而实现与binder驱动的交互来间接的实现跨进程通信。

在这里插入图片描述

Client,Server,ServerManager,Binder驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server),客户端(Client),DNS域名服务器(ServiceManager)以及路由器(Binder驱动)之间的关系。
通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如www.google.com然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此首先要通过CNS域名服务器,域名服务器中保存了www.google.com对应的ip地址10.249.23.13,然后通过这个ip地址才能找到www.google.com对应的服务器。

在这里插入图片描述

Android Binder设计与实现(https://blog.csdn.net/universus/article/details/6211589)一文中对Client,Server,Servicemanager,Binder驱动有很多详细的描述,以下是部分摘录:

5.2 Binder通信过程
至此,我们大致能总结出Binder通信过程:
1.首先,一个进程使用BINDER_SET_CONTEXT_MGR命令通过Binder驱动将自己注册成为ServiceManager。
2.Server通过驱动向ServiceManager中注册Binder(Server中的Binder实体),表明可以对外提供服务。驱动为这个Binder创建位于内核中的实体节点以及ServiceManager对实体的引用,将名字以及新建的引用打包传给ServiceManager,ServiceManager将其填入查找表。
3.Client通过名字,在Binder驱动的帮助下从ServiceManager中获取到Binder实体的引用,通过这个引用就可以实现和Server进程的通信。

在这里插入图片描述

5.3 Binder通信中的代理模式
我们已经解释清楚Client,Server借助Binder驱动完成跨进程通信的实现机制了,但是还有个问题会放我们困惑。A进程想要B进程中某个对象(Object)是如何实现的?毕竟它们分属不同的进程,A进程没法直接使用B进程中的object。

前面我们介绍过跨进程通信的过程都有Binder驱动的参与,因此在数据流经过Binder驱动的时候驱动会对数据做一层转换。当A进程想要获取B进程的object对象时,驱动并不会真的把object对象返回给A,而是返回一个跟object对象一模一样的代理对象objectproxy,这个objectproxy具有和object一模一样的方法,但是这些方法并没有B进程中object对象哪些方法的能力,这些方法只需要将请求参数交给驱动即可。对于A进程来说和直接调用object中的方法是一样的。

当Binder驱动接收到A进程的消息后,发现这是个objectproxy就去查询自己维护的表单,一查发现这是B进程object的代理对象。于是就去通知B进程调用object的方法,并要求B进程把请求结果发给自己。当驱动拿到B进程的返回结果后就会转发给A进程,一次通信就完成了。

在这里插入图片描述

六.手动编码实现跨进程调用
通常我们在做开发时,实现进程间通信用的最多的就是AIDL。当我们定义好AIDL文件,在编译时编译期会自动帮生成代码实现IPC通信。借助AIDL编译以后的代码能帮助我们进一步理解Binder IPC的通信原理。
但是无论是从可读性还是可理解性上来看,编译期生成的代码并不友好。比如BookManager.aidl文件对应会生成一个BookManager.java文件,这个java文件包含了一个BookManager接口,一个Stub静态的抽象类和一个Proxy静态类。proxy是stub的静态内部类,stub是bookmanager的静态内部类,这就造成了可读性和理解性的问题。
那么android为何要这么设计呢?这是因为当有多个aidl文件的时候把bookManager,stub,proxy放在同一个文件里能有效的避免stub和proxy重名的问题。
因此便于理解,我们手动编写代码来实现跨进程通信。

6.1 各java类职责描述
在正式编码实现跨进程调用之前,先介绍下实现过程中用到的一些类。了解这些类的职责,有助于我们更好的理解和实现扩进程通信。
IBinder:IBinder是一个接口,代表一种跨进程通信的能力。只要实现了这个接口,这个对象就能跨进程传输。
IInterface:代表的就是Server进程对象具备什么样的能力(能提供哪些方法,其实对应的就是aidl文件中定义的接口)
Binder:java层的binder类,代表的其实就是Binder本地对象。BinderProxy类是Binder类的一个内部类,它代表远程进程的Binder对象的本地代理,这两个类都继承自IBinder,因而都具有跨进程传输的能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值