从事android工作一年了,感觉要学习的东西真的太多了,并且android的前景也不容乐观,最近在看项目时,一直使用Bundle,也知道是Binder通信,但是Binder是什么,什么原理,我真的不知道。感觉Binder是在Android中出现频率很高的一个名词,提到IPC,无论如何都绕不开Binder,深入理解binder需要一定的积累,感觉把binder理解的滚瓜乱熟,就能跳槽了,哈哈哈哈哈,今天找时间看一些网上的博客,帖子,浅浅的学习一下Binder。
1.Binder的背景
Binder是Android系统中的一种跨进程通信(IPC)机制,它是由Android的创始人之一安迪·鲁宾(Andy Rubin)在Google时期设计的。Binder最初是为了解决Android系统中进程间通信
的需求而开发的。
Android系统是基于Linux内核的,在Linux内核中,每个应用程序在Android系统中运行在独立的进程中。
为了实现应用程序
之间的通信和数据共享
,Android需要一种高效且安全的跨进程通信机制
。
Binder的设计灵感来自于Object Request Broker(ORB)模型,它是一种用于分布式系统中的对象请求代理模型。Binder基于这一思想,在Android系统中实现了一套轻量级的进程间通信机制。
2.Binder的演进过程
从背景可以看出来,binder是Android的特有的一种IPC框架,那么为什么不直接使用Linux的本身的跨进程通信的框架呢?是内存太大?还是Android不支持? 这里我参考了hackmd.io/@AlienHackM... && zhuanlan.zhihu.com/p/35519585
图就不自己画了。
2.1 Linux的内存缺陷
- 首先了解下几个linux名词:
进程隔离
、进程空间划分
、系统调用
.
-
进程隔离
简单的说就是操作系统中,进程与进程间内存是不共享的。两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,这就是进程隔离的通俗解释。A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。
-
进程空间划分:用户空间(User Space)/内核空间(Kernel Space)
内核空间是操作系统内核访问的区域,独立于普通的应用程序,是受保护的内存空间。其实早期操作系统是不区分内核空间和用户空间的,但是应用程序能访问任意内存空间,如果程序不稳定常常把系统搞崩溃,比如清除操作系统的内存数据。后来觉得让应用程序随便访问内存太危险了,就按照CPU 指令的重要程度对指令进行了分级, 指令分为四个级别 :Ring0~Ring3 (和电影分级有点像),linux 只使用了 Ring0 和 Ring3 两个运行级别,进程运行在 Ring3 级别时运行在用户态,指令只访问用户空间,而运行在 Ring0 级别时被称为运行在内核态,可以访问任意内存空间。
-
系统调用:用户态/内核态 用户态跟内核态的定义和进程空间的划分是一一对应的。当进程/线程运行在内核空间时就处于内核态,而进程/线程运行在用户空间时则处于用户态。
- Linux中IPC原理
上面这个图很清晰的表达了Linux的通讯原理,先介绍下图中的两个函数,
copy_from_user()
将数据从用户空间拷贝到内核空间
copy_to_user()
将数据从内核空间拷贝到用户空间
可以看到蓝色部分是进程A的通过copy_from_user()将数据从用户的内存拷贝到内核缓存中。而从绿色的可以看到,当进程C想使用A中的数据时,通过copy_to_user()
将数据从内核缓存中拷贝到用户进程的内存缓存中(也就是用户空间)。
- 可以看到在跨进程通信过程中,数据经过2次拷贝,频繁的用户态和内核态切换,十分影响性能
- 进程C无法确认进程A传递的数据大小,所以会尽可能打的开启内存空间,这就可能导致了内存浪费。
2.2 Android中binder机制
上面了解了Linux中的跨进程通信(IPC)的缺点,Android手机的内存在初期是很小的,肯定比电脑的小,所以,Google工程师,就针对上面Linux的跨进程通信的弊端,开启了Binder之旅。
既然是优化,肯定是利用了某个技术,减去或者优化了某个环节。在了解是怎么优化之前,我们需要先了解Binder用到的几个技术名词:动态内核可加载模块
,内存映射
-
动态内核可加载模块(Loadable Kernel Modules,LKM) 是一种允许在运行时加载和卸载的内核模块。在Android系统中,Binder作为一种进程间通信(IPC)机制,它在内核层面起着重要的作用。
- 功能扩展和定制化:通过加载动态内核可加载模块,可以向Android系统添加新的功能或修改现有功能。在Binder中,可以通过加载LKM来扩展Binder的功能,实现更多的通信特性或者优化通信性能。
- 驱动程序支持:一些硬件设备的驱动程序可能需要在运行时加载到内核中,以便在系统启动后提供相应的功能和服务。这些驱动程序可能与Binder通信,因此加载到内核中的LKM可以通过Binder与用户空间的进程进行通信。
- 安全性和稳定性:动态内核可加载模块允许在运行时加载和卸载,这意味着可以根据需要添加或移除功能,而无需重新编译整个内核。这种灵活性有助于提高系统的安全性和稳定性,因为可以随时根据需求调整系统的功能。
在binder中的作用:
binder作为Android特有的机制,并不是Linux内核本身就包含的机制,而动态内核可加载模块的作用就是做到了插件的作用,动态加载、新增功能、不需要重新编译整个内核
优点。 -
内存映射 (Memory Mapping)是一种将磁盘上的文件映射到进程的内存空间的技术。它允许
进程
直接访问文件的内容,而无需通过传统的读取和写入操作。在内存映射中,操作系统
会将文件的一部分或整个文件映射
到进程的地址空间中的一段内存区域。一旦文件被映射到内存中
,进程就可以像访问内存一样访问文件
,而不必
像传统的文件I/O操作那样进行读取和写入。
怎么样,看完上面两个技术点,就可以猜到Binder相对于传统的Linux的IPC通信的优化之处应该是在哪里了。这么看的话态内核可加载模块
要是对于Binder在Android中使用做了功能支持
,内存映射
是针对传统的IPC通信做了功能优化
。
Binder的IPC通信是这样的(直接粘贴了) 从图中可以一眼看出来的是:Binder的机制跟传统的IPC不同的地方是,内核空间开辟了两个内存。 首先 Binder 驱动在内核空间创建一个数据接收缓存区; 接着在内核空间开辟一块内核缓存区,建立内核缓存区 和内核中数据接收缓存区 之间的映射关系,以及内核中数据接收缓存区 和接收进程用户空间地址的映射关系;
- 最左边跟传统的IPC通信还是一样的,都是要将进程A的数据通过
copy_from_user()
拷贝到内核中(注意这个内核是动态内核可加载模块) - 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区 和内核中数据接收缓存区 之间的映射关系,以及内核中数据接收缓存区 和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用 copyfrom user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
OK,这样一个超越传统IPC通信的架构就出现了。
2.Binder在Android中的使用
Binder在Android系统中被广泛用于进程间通信(IPC)和组件间通信,它提供了一种安全而高效
的机制来实现不同应用程序
或不同进程
之间的数据传输和方法调用。以下是在Android开发中使用Binder的一些常见场景和方法:
- 跨进程通信(IPC) :Binder允许不同进程之间的通信,包括进程之间的数据传输、方法调用等。在Android开发中,可以使用Binder实现进程间的通信,比如Activity和Service之间的通信、应用程序和系统服务之间的通信等。
- AIDL(Android Interface Definition Language) :AIDL是Android中定义Binder接口的一种语言,它用于描述Binder接口的方法和参数。通过编写AIDL文件,可以定义一个接口,然后让客户端和服务端基于这个接口进行通信。
- Service:在Android中,Service是一种可以在后台执行长时间运行操作的组件,它可以与其他组件或后台进程进行通信。通过继承Service类并实现其中的onBind()方法,可以创建一个可以绑定的服务,并通过Binder对象提供对服务的访问。
- 客户端-服务器模式:Binder可以用于实现客户端-服务器模式的通信。在这种模式下,服务端提供了一个Binder对象,客户端通过Binder对象调用服务端提供的方法或获取数据。
- HandlerThread:HandlerThread是Android中的一个线程类,它继承了Thread类并实现了Handler,可以用于处理异步任务。在HandlerThread中,可以使用Binder来进行线程间的通信,实现数据的传输和任务的调度。
3.Binder通信的原理(宏观)
前面了解了,微观上的Binder的原理,面试的知识点已经get了一半了,类似于我们了解了什么是HTTP,那么后面我们应该了解下,前端和服务端是怎么样通过http通信的呢?
首先我们先猜想一下,进程通信,肯定是双向的,双向通信就设置消息的发送接收了,就跟我们进出停车场,肯定有一个管理系统。要不停车场满了或者进出口堵塞了都没有感知系统。那么在Binder进程通讯中肯定也有一个管理系统,来处理消息的队列。大体架构应该是这样。
猜想图
大体框架应该是这样,但是每个框架都处理一些特殊情况,所以可能是有一些小变动,但是也是以这个演进的,废话不多说,去网上直接Google下,看看详细的框架是怎么样的。
知乎上的图
这个图记起来比较麻烦,实例化一下,就是你家里的Wifi,现在我们按照在家里使用wifi
的步骤来分析下,binder在IPC通讯的过程。
- Binder 驱动 (
安装路由器
)
跟我们使用wifi时要有一个路由器负责通讯以及管理设备一样,Binder驱动就是整个binder在IPC中的核心;驱动负责进程之间的 Binder 通信建立
,Binder 传递
,引用计数管理
,数据包的传递
和交互
等一系列底层支持(怎么样,这种双向通讯或者说有数据通讯的机制,肯定有一个关键的管理器)。
- ServiceManager 与实名 Binder (
路由器注册到手机Wifi列表
)
当我拿着开手机去蹭网时,打开wifi,手机列表一般有很多个wifi名字,我们选择想要链接名字的Wifi,那么这一个列表的Wifi是怎么来的呢?
ServiceManager 和手机Wifi管理器
类似,作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用(手机Wifi列表根据名字直接找到对应的Wifi,不需要Wifi地址)。注册了名字的 Binder 叫实名 Binder(实名老王家的Wifi
)。 Server 创建了 Binder,并为它起一个字符形式,可读易记得名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager(路由器注册好wifi后,注册到手机Wifi连接页面
),这样我们就知道一个Wifi名字为老王家的Wifi,它位于老王家。
驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager。ServiceManger 收到数据后从中取出名字和引用填入查找表。
细心的读者可能会发现,ServierManager 是一个进程,Server 是另一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDERSET CONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client。
- Client 获得实名 Binder 的引用
手机连接对应wifi
前面已经知道了,小明,小强,老王家的wifi名称是怎么出现在我的手机列表里的。现在我就可以通过名称直接连接到他们的真正的WIFI地址。
Server 向 ServiceManager 中注册了 Binder 以后, Client(我的手机) 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder: 我申请链接老王家的Wifi
的时,手机wifi模块就找到老王家的wifi对应的真正地址,然后发起连接请求。
连接对应的Binder 引用跟上面也是一致的。ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的 Client。
从面向对象的角度看,Server 中的 Binder 实体现在有两个引用:一个位于 ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样。
3.2 Binder 通信过程
上面是一个更为详细的binder IPC通讯架构。
其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供
,而 Client、Server 由应用程序来实现
。Client、Server 和 ServiceManager 均是通过系统调用
open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。
需要重点了解的就是
:
-
首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成ServiceManager;
-
Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
-
Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
但是从上面的图中我们可以看到在蓝色跟最顶层之间,还是在framework层,有一层为BinderProxy,他的作用是什么呢?
其实这个跟我们在serviceManger中注册的service是一回事,一个是利用映射,一个是利用代理。我们在Client中使用的Binder对象,也是server中的一个代理。binder此处的设计,也是突出了代理模式的优点。就像上面我们只管调用binder,我们并不需要知道代理怎么实现的。
- 你可以在客户端毫无察觉的情况下控制服务对象。
- 如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。
- 即使服务对象还未准备好或不存在, 代理也可以正常工作。
- 开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理。
这篇文章断断续续整理了好几天,理论部分就到这里吧。实践部分大家网上看下代码就可以。主要的是这个思想和原理。总结就是
- 两次内存复制变为一次
- 利用动态内存块和分页映射原理实现通讯。
- 还有就是几个关键名词以及在binder中的作用,(binder驱动(中枢)、clinent,server、serviceManger(管理存储))