漫聊Android Binder那些事

前言

前两个月去了祖国的三个城市旅游,分别是呼伦贝尔、重庆、三亚,感受下了下祖国的大好山河,舒服惬意。读万卷书,行万里路。有机会和还是要多出去看看外面的世界,长长见识,这些旅行阅历对个人的成长是非常有帮助的。这段时间的旅游,最大的收获就是感受大自然的美景,草原、山川、大海。也刷新了我的旅游空间范围,目前,去过的最东边是大阪(日本)、最西边是重庆、最北边是呼伦贝尔、最南边是三亚,后续有时间我也会写点“游记”分享下。
频繁的旅游,让我在专业学习上荒废了不少,看到之前定制的计划的月度计划没有完成,倍感罪孽深重。那么就在这个国庆假期弥补下吧。

引子

关于Binder,网上的文章非常的多,我个人也是看过很多博客和书籍(例如《Android 进阶指北》、《Android 开发艺术探索》),以及我的一些同事做的分享。大致分类就是简单的和复杂的。简单的就是聊聊概念和原理,感觉就像喝鸡尾酒,复杂的就是深入源码,感觉就像喝酱香茅台。这些都挺好的,我这里说的好,是指适合自己的需求就是好。目前,学习Binder的同学有两种需求:

  1. 面试
  2. 极客

对于第一种需求,如果深入看Binder的源码,想必是一种煎熬,我这并不是说因为面试就不用看Binder的源码,而是觉的没有必要太过深入(对于应用层的开发来说),对于Binder,我觉的懂原理就够了,因为它很偏底层,没必要在这上面花太多精力,除非你是做底层的开发,那么Binder面试就必须要深入了。对于第二种,极客,那必须要深入源码的世界遨游了,横跨Java层、Native层,能搞清楚里面的所有细节的大神,我非常佩服。

我的这篇文章探讨Binder就属于第一种需求,也试着做后者深入学习的引子,内容不是很深,没有过多的源码分析。当然,也不要指望用这一片文章就能应付Binder的相关面试,鬼知道面试官有没有恶趣味,不按套路出牌,所以看源码可以,但请不要钻牛角尖而无法自拔。

一、IPC方案介绍

大家都知道,使用Binder可以实现跨进程通信,请问,如果我们不使用Binder,能不能实现跨进程通信?能,必须能。

  1. Android系统 和Binder本身就是两个东西。Binder是基于开源的OpenBinder实现的,并不是Google 开发的,只是Binder的作者后来入职Google,然后就把自己的杰作融入到了Android系统中了。
  2. Android 是基于Linux系统开发的,Linux支持的那些的好玩意Google向来不想重复造轮子。比如管道、信号量、消息队列、共享内存、套接字。

对于应用层的开发来说,上面的那些,知道就好,真正使用跨进程通信,咱基本用不到。因为Google觉的,这些玩意用起来太麻烦了,我干脆自己也做几个轮子,方便用吧。于是,Binder、ContentProvider、AIDL、Messager就诞生了。作为重头戏,Binder我放在后面讲,先聊聊前面几个。

我在自己负责的项目中,是有需要做IPC的,比如宿主需要向插件要一些书籍数据、账号数据等。前面提到的那些IPC,我先简单介绍下:

  1. ContentProvider:作为四大组件中最没有存在感的家伙,前两年大家用的都很少,主要就是用来读通信录啥的(应用场景不多,很多人都没有用过),最近几年这家伙出镜频次呈指数量增长。主要是近几年插件化、组件化比较火,导致跨进程通信的场景和需求倍增,而ContentProvider 就是为IPC机制而生的。还有就是Jetpack 里新出的启动框架 App Startup,核心实现用的就是ContentProvider(关于App Startup,网上有很多文章,这里不做多余的介绍)。ContentProvider之所以能实现IPC,底层的实现就是用的Binder那套玩意。关于ContentProvider的使用,这里我就不贴代码了,网上一大堆。这里我需要提两点,一个是在使用ContentProvider时,注意Uri 的书写规范,这个千万别写错了。还有一个就是ContentProvider 的初始化时机非常的早,和其他组件完全不一样,应用启动时,ContentProvider初始化(onCreate)是在Application生命周期方法attachBaseContext之后、onCreate之前,使用不当会产生非常奇怪的问题,我去年就被微信的一个ContentProvider died导致我们应用一启动就崩溃的问题折磨了一个多星期没有睡好觉(抱歉,有点跑题了。。。)具体的使用方式和实现demo网上也有很多,这里我也不啰嗦了。ContentProvider 使用介绍链接——》
  2. AIDL :即Android 接口定义语言。这个家伙功能相当强大,既可以实现跨进程数据传输,也能满足跨进程的方法调用。但这个家伙使用起来比较麻烦,因为要创建规范的aidl的文件路径和文件,我在平时开发中没有使用过,但看到其他同事在特殊场景还是用的,比如我前面提到的跨进程的方法调用。具体的使用方式和实现demo网上也有很多,这里我也不啰嗦了。AIDL 使用介绍链接——》
  3. Messager :信使。这个家伙也可以实现IPC,但实现方式相比前面两个来说是非常简洁的,底层使用的AIDL ,因此也可说是对AIDL 的封装吧,非常适合两个进程间简单的数据交互,当然局限也是有的,就是不能直接实现跨进程的方法调用,不过这不是问题,既然无法直接实现,那就间接中转下撒。具体的使用方式和实现demo网上也有很多,这里我也不啰嗦了。Messager 使用介绍链接——》

这里再补充一点知识,就是跨进程通信所需要的载体。对应大对象,务必是需要序列化的,这里就需要使用Bundle。这个如果大家做过两个Activity之间传数据的,一定用过它。关于它,这里我点到为止。

二、Linux那点你必须知道的事

在正式开讲Binder前,我认为必须要先说下Linux内存管理的知识,这部分学过大学计算机操作系统的同学一定有所了解,这里我不想啰嗦太多,扯一堆概念,只说你必须要知道且理解的知识。

1、首先就是要理解“内核空间”和“用户空间” 是什么?

先看一张图

在这里插入图片描述

为了保护用户进程,不能直接操作内核,以保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间和内核空间。Linux操作系统将较高的1GB供内核使用,称为内核空间,将较低的3GB供各进程使用,称为用户空间。内核空间是Linux内核的运行空间,用户空间是用户程序的运行空间。为了保证内核的安全,它们是隔离的,即使用户的程序崩溃了,内核也不会受到影响。内核空间的数据是可以进程间共享的,而用户空间的数据则不可以。看图进程A的用户空间是不能和进程B的用户空间共享的。

在继续看这张图,两个进程要相互通信,就需要借助内核空间作为通信桥梁,然而用户空间要想访问内核空间,就必须要系统调用来实现,即所有资源的访问都是在内核的控制下进行,原因就是防止用户程序对系统资源越权访问,提升系统的安全性稳定性

2、Linux 的IPC通信

前面提到用户空间需要系统调用才能访问内核空间,那如何访问 ?
先来祭出一张图:
在这里插入图片描述
在Linux系统中,两个进程要相互通信,需要如下两个操作:

  1. copy_from_user : 将用户空间的数据复制到内核空间。
  2. copy_to_user : 将内核空间的数据复制到用户空间。

具体来说就是内核程序在内核空间分配内存并开辟-块内核缓存区, 发送进程通过copy from user函数将数据复制到内核空间的缓冲区中。同样,接收进程接收数据时在自己的用户空间开辟一块内核缓存区,然后内核程序调用copy_ to _user()函数将数据从内核缓存区复制到接收进程。这样数据发送进程和数据接收进程就完成了一一次数据传输, 也就是一次进程间的通信。

结合图以及我前面的描述,就会发现Linux的IPC有一个问题,就是一次数据通信,需要做两次复制操作,即用户空间——》内核空间——》用户空间,效率有点低。Google 觉的这效率不行,要换,于是想到新入职的哥们做的Binder据说在IPC方面效率不错,咱可以用上。

3、内存映射机制

即把设备地址映射到虚拟内存区。在Linux中系统调用mmap函数来实现内存映射。将用户空间的一块内存区域映射到内核空间。建立映射关系后,用户对这块内存区域的修改可以直接反应在内核空间,反之亦然。内存映射可以减少数据的复制次数

三、Binder 原理

终于到了万众瞩目的章节,有了前面的知识铺垫,接下来理解Binder就不是那么的费事了(啃源码除外),接下来我会从两个方面来讲解Binder。

1、 Binder 通信模型概述

在这里插入图片描述
为了提升IPC的效率,Linux系统有一套内存映射机制,因为可以减少数据的复制次数,所以效率非常高,Binder就是基于此实现。由于内存映射通常是在有物理介质的文件系统上,然而Binder没有物理介质,因此它使用内存映射来实现跨进程的传输数据。结合上面这张图。Binder 的通信步骤如下:

  1. Binder 驱动在内核空间创建一个数据接收缓存区。
  2. 在内核空间开辟块内核缓存区,建立内核缓存区和数据接收缓存区之间的映射关系,以及数据接收缓存区和接收进程用户空间地址的映射关系
  3. 发送方进程通过copy from user()函数将数据复制到内核中的内核缓存区,由于内核缓存和收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

整个过程只使用了一次复制, 不会因为不知道数据的大小而浪费空间或者时间,这样效率更高。

2、 Binder 系统架构模型详解

根据上面一小节的图,我们能看出发送进程和接收进程的通信很像一个CS模型,即客户端/服务端。这里可以做一个很相近的类比,就是Binder通信过程很像我们TCP/IP 。Binder系统由四个部件组成:

  1. Binder驱动 :即把请求信息投递到对方所在的进程中。作用上有点类似于路由器。
  2. ServiceManager :记录注册注册的Binder服务。作用上有点像DNS。
  3. Binder Client:发送进程。
  4. Binder Server:接收进程。

用一张图来说:
在这里插入图片描述
接下来我将对上 Binder驱动和ServiceManager做分析,里面会涉及到一些C 代码,但不会太多,请安心食用。

1. Binder 驱动

我们知道,Android 系统是基于Linux内核的,因而它所依赖的Binder驱动也必须是一个标准的Linux驱动。具体而言,Binder Driver 会将自己注册成一个 misc device, 并向上层提供一个/devbinder节点一值得一提的是, Binder 节点并不对应真实的硬件设备。Binder 驱动运行于内
核态,可以提供open(), ioctl(), mmap()等常用的文件操作。其中**mmap()**是我们接下来要重点分析的。

  1. binder_open() : 作用就是打开Binder驱动,这个没啥好分析的。
  2. binder_mmap() : 前面我在讲内存映射时提到过这个指令,作用就是实现内存映射,即:把设备指定的内存块直接映射到应用程序的内存空间中。

在这里插入图片描述
对于应用程序而言,它通过mmap()返回值得到一个内存地址(当然这是虚拟地址),这个地址通过虚拟内存转换(分段、分页)后最终将指向物理内存的某个位置。对应Binder驱动而言,它也是一个指针(binder_proc_buffer),指向某个虚拟内存地址,经过虚拟内存转化后,它和应用程序中指向的物理内存处于同一个位置。
经过上面一顿关于地址的映射,这时的Binder和应用程序就拥有了若干共用的物理内存块,即它们对各自的内存地址操作,实际上是在同一块内存中执行的。若还不理解,那看看下面这张图。
在这里插入图片描述
Binder 驱动通过copy_ from user(),进程A中的某段数据复制到其binder proc->buffer所指向的内存空间中。因为binder proc->buffer 在物理内存中的位置和进程B是共享的,因而进程B可以直接访问到这段数据。也就是说,Binder驱动只用了次复制,就实现了进程A和B间的数据共享。
由于binder_mmap()源码非常多,这里我节选了一些。并加上注释。

static int binder. mmap (truct file *filp, struct Vm area struct *vma) {
int ret;
struct vm_ struct *area;

struct binder_ proc *proc = filp->private_ data; // 取出对应进程的binder_ proc
const char *failure_ string;
struct binder_ buffer *buffer;
// 计算虚拟块大小等:
if (binder_ update_ page_ range (proc, 1, proc->buffer, proc->buffer + PAGE_ SIZE, vma))
{
ret = - ENOMEM;
failure string = "alloc small buf";
goto err_ alloc_ small_ buf_ failed;
}
// 应用程序申请的内存最大支持4M。所以这也说明Binder不适合传递大数据!!!
if ( (vma->vm end -vma->vm_ start) >SZ_ 4M)
vma- , vm end = vma->vm start + SZ 4M;

..............

2. ServiceManager

这部分内容非常复杂,也非常的多, 网上的资料看的让人也是头疼不已。为此,我决定抛弃绝大数细节,让大家看看这个家伙的骨架,即核心功能。
前面我将ServiceManager 比喻成DNS,其实它和DNS真的很像,ServiceManager管理者所有的Binder 服务。因此它有如下几个功能:

  1. 注册:当一个Binder Server创建后,它需要将自己的名称(Binder句柄)对应关系注册到ServiceManager中。
  2. 查询:即应用可向ServiceManager发起查询请求,以此来获知某个Binder Server所对应的句柄。

总结下:ServiceManager 的功能架构——内部维护着一个svclist列表,用于存储所有的Server相关的信息,查询和注册都是基于此展开的。

关于ServiceManager,想要展开讲细了,内容会超级的多,有ServiceManagerProxy、IBinder 、BpBinder等。后续有时间我可以单独出一篇文章聊聊

四、总结:

Binder 是一个非常精深的东西,内容横跨Native、Java、Linux操作系统等一堆知识。大家学习时,还是按照自己的实际情况和需求吧。

参考:

1、《深入理解Android 内核设计思想》——林学森
2、《Android 进阶指北》——刘望舒

猜你喜欢

转载自blog.csdn.net/qq_26439323/article/details/108911645