全面总结Android面试知识要点:Framework内核解析面试题

请点赞,你的点赞对我意义重大,满足下我的虚荣心。
🔥常在河边走,哪有不湿鞋。或许面试过程中你遇到的问题就在这呢?
🔥关注我个人简介,面试不迷路~

一、Android中多进程通信的方式有哪些?

这道题想考察什么?

对计算机中进程的了解,为了完成跨进程通信需要解决的问题以及现有的跨进程通信方式大致实现原理与区别

考察的知识点

操作系统内存、进程与通信

考生应该如何回答

总的来说,进程间通信方案有很多他们分别是:管道,信号,信号量,内存共享,socket,binder,消息队列,但是使用最多的还是binder,尤其是用户空间的跨进程通信,基本大多采用的是binder。

进程隔离

操作系统有虚拟内存与物理内存的概念。物理内存指通过物理内存条而获得的内存空间,而虚拟内存则是计算机系统内存管理的一种技术,虚拟内存并非真正的内存,而是通过虚拟映射的手段让每个应用进程认为它拥有连续的可用的内存。在使用了虚拟存储器的情况下, 通过MMU(负责处理CPU的内存管理的计算单元)完成虚拟地址到物理地址的转换。

image.png

程序使用的虚拟内存被操作系统划分成两块:用户空间和内核空间。用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方,内核空间由所有进程共享。为了安全,内核空间与用户空间是隔离的,这样即使用户的程序崩溃了,内核也不受影响。同样为了安全,不同进程的各自的用户空间也是隔离的,这样就避免了进程间相互操作数据的现象发生,从而引起各自的安全问题。不同进程基于各自的虚拟地址不同,从逻辑上来实现彼此间的隔离。

image.png

IPC通信

为了能使不同的进程互相访问资源并进行协调工作,需要在不同进程之间完成通信。而通过进程隔离可知不同进程之间无法直接完成通信的工作。此时就需要特殊的方式来实现:IPC(Inter-Process Communication )即进程间通信。不同进程存在进程隔离,但是内核空间被所有进程共享,因此绝大多数的IPC机制就利用这个特点来实现通信的需求。因为Android是在Linux内核基础之上运行,因此Linux中存在的IPC机制在Android中基本都能使用。

管道

管道是UNIX中最古老的进程间通信形式,它实际上是由内核管理的一个固定大小的缓冲区。管道的一端连接一个进程的输出,这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。

image.png

可以通过pipe创建一个管道:

//匿名管道(PIPE)
#include <unistd.h>
int pipe (int fd[2]); //创建pipe
ssize_t write(int fd, const void *buf, size_t count);   //写数据
ssize_t read(int fd, void *buf, size_t count);     //读数据

pipe创建的是匿名管道,它存在以下限制:

1、大小限制(一般为4k)

2、半双工(同一个时刻只数据只能向一个方向流动,需要双方通信时,需要建立两个管道 )

3、只支持父子和兄弟进程之间的通信

另外还有FIFO实名管道,支持双向的数据通信,建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,但是要同时和多个进程通信它就力不从心了。需要了解更多关于管道机制的内容可以在腾讯课堂搜索享学课堂。

信号

信号主要用于用于通知接收进程某个事件的发生。信号的原理是在软件层次上对中断机制的一种模拟,一个进程收到一个信号与处理器收到一个中断请求是一样的。

信号是一种异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。也就是说信号接收函数不需要一直阻塞等待信号的到达。 进程可以通过sigaction注册接收的信号,就会执行响应函数,如果没有地方注册这个信号,该信号就会被忽略。

int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
//根据参数signum指定的信号编号来设置该信号的处理函数。
//参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。

在Android中,如果程序出现ANR问题会发出:SIGNALQUIT 信号,应用程序可注册此信号的响应实现监听ANR,爱奇艺xCrash,友盟+ U-APM、腾讯Matrix都实现了该方式。可以在腾讯课堂搜索享学课堂了解更多ANR监控相关内容。

信号量

信号量实际上可以看成是一个计数器,用来控制多个进程对共享资源的访问。它不以传送数据为主要目的,主要作为进程间以及同一进程内不同线程之间的同步手段。

信号量会有初值(>0),每当有进程申请使用信号量,通过一个P操作来对信号量进行-1操作,当计数器减到0的时候就说明没有资源了,其他进程要想访问就必须等待,当该进程执行完这段工作(我们称之为临界区)之后,就会执行V操作来对信号量进行+1操作。

共享内存

通过进程隔离可知,不同进程基于各自的虚拟地址不同,从逻辑上来实现彼此间的隔离。 而共享内存则是让不同进程可以将同一段物理内存连接到他们自己的地址空间中,完成连接的进程都可以访问这块共享内存中的数据。

image.png

由于多个进程共享同一块内存区域,所以通常需要用其他的机制来同步对共享内存的访问,如信号量 。而在Android中提供了独特的匿名共享内存Ashmem(Anonymous Shared Memory)。Android的匿名共享内存基于 Linux的共享内存,都是在临时文件系统上创建虚拟文件,再映射到不同的进程。 它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于 Linux 的共享内存,Ashmem 对内存的管理更加精细化,并且添加了互斥锁。

在开发中,可以借助Java中的 MemoryFile 使用匿名共享内存,它封装了 native 代码。 Java 层使用匿名共享内存的步骤一般为:

  1. 通过 MemoryFile 开辟内存空间,获得ParcelFileDescriptor;
MemoryFile memoryFile = new MemoryFile("test", 1024);
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);
ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(des);

2. 将 ParcelFileDescriptor 传递给其他进程;
3. A进程往共享内存写入数据;

memoryFile.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});

4. B进程从共享内存读取数据。

ParcelFileDescriptor parcelFileDescriptor;
FileDescriptor descriptor = parcelFileDescriptor.getFileDescriptor();
FileInputStream fileInputStream = new FileInputStream(descriptor);
fileInputStream.read(content);

在第二步中一般的利用Binder机制进行FD的传输,传输完成后,就可以直接借助FD完成跨进程数据通信,而且没有内存大小的限制。因此当需要跨进程进行大数据的传递时,可以借助匿名共享内存完成!

在Android中视图数据与SurfaceFlinger的通信、腾讯MMKV、Facebook Fresco等等技术都有利用到匿名共享内存。

消息队列

消息队列是一个消息的链表,存放在内核中并由消息队列标识符标识。它克服了Linux早期IPC机制的很多缺点,比如消息队列具有异步能力,又克服了具有同样能力的信号承载信息量少的问题;具有数据传输能力,又克服了管道只能承载无格式字节流以及缓冲区大小受限的问题。

但是缺点是比信号和管道都要更加重量,在内核中会使用更多内存,并且消息队列能传输的数据也有限制,一般上限为两页 16kb。

int msgget(key_t, key, int msgflg); //创建和访问消息队列
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);  //发送消息
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg); //获取消息

受限于性能,数据量等问题的限制,Android系统没有直接使用Linux消息队列来进行IPC的场景,但是有大量的场景都利用了消息队列的特性来设计通信方案,比如进行线程间通信的Handler,就是一个消息队列。

socket

socket 原本是为网络通讯设计的,但后来在 socket 的框架上发展出一种 IPC 机制,就是 UNIX domain socket。虽然网络 socket 也可用于同一台主机的进程间通讯(通过 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。 在Android系统中,Zygote进程就是通过LocalSocket(UNIX domain socket)接收启动应用进程的通知。

socket(AF_INET, SOCK_STREAM, 0); // 对应java  Socket
socket(AF_UNIX, SOCK_STREAM, 0);// 对应java  LocalSocket
Binder

Android Binder源于Palm的OpenBinder,在Android中Binder更多用在system_server进程与上层App层的IPC交互。 在Android中Intent、ContentProvider、Messager、Broadcast、AIDL等等都是基于Binder机制完成的跨进程通信。

总结

Android是在Linux内核基础之上运行,因此Linux中存在的IPC机制在Android中基本都能使用,如:

  1. 管道: 在创建时分配一个page大小的内存,缓存区大小比较有限;
  2. 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;
  3. 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  4. 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
  5. 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
  6. 套接字:作为更通用的接口,传输效率低;
为什么Android选择Binder作为应用程序中主要的IPC机制?

Binder基于C/S架构,进行跨进程通信数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能虽然比管道等方式好,但是不如共享内存。

但是传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,只能由使用者在传递的数据包里填入UID/PID,伪造身份非常简单;而Binder不同,可靠的身份标记只有由IPC机制本身在内核中添加,binder就是这么做的,不由用户应用程序控制,直接在内核向数据中添加了进程身份标记。

因此综合考虑,Binder更加适合system_server进程与上层App层的IPC交互。

二、描述下Binder机制原理?(东方头条)

这道题想考察什么?

在Android中广泛运用的Binder机制是如何实现跨进程通信的

考察的知识点

Binder原理、内存映射

考生应该如何回答

Binder概述

Binder是Android提供的一套进程间相互通信框架,它是一种效率更高、更安全的基于C/S架构的IPC通信机制,其本质也是调用系统底层的内存共享实现。它基于开源的 OpenBinder 实现,从字面上来解释 Binder 有胶水、粘合剂的意思,顾名思义就是粘和不同的进程,使之实现通信。

Binder实现机制

进程隔离

进程隔离不懂的同学可以先阅读8.1章节的进程隔离介绍。

通过进程隔离可知,不同进程无法直接通信,但是内核空间是被不同进程共享。那么是不是可以将数据从数据发送方进程用户空间交给内核,再由内核传递至数据接收方用户空间从而完成数据的交互,实现通信呢? 没错,Linux下传统的IPC机制正是借助了这点从而实现了跨进程通信。由于进程用户空间与内核空间同样存在隔离,无法完成数据在用户空间与内核空间直接传递,所以现在解决跨进程通信就变成了解决用户空间与内核空间数据传递的问题。

进程空间划分:用户空间(User Space)/内核空间(Kernel Space)

现在操作系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限。为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。

简单的说就是,内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。

image.png
系统调用:用户态与内核态

虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。

Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。

当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态) 。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态) 。此时处理器在特权级最低的(3级)用户代码中运行。

系统调用主要通过如下两个函数来实现:

copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间

Linux 下的传统 IPC 通信原理

理解了上面的几个概念,我们再来看看传统的 IPC 方式中,进程之间是如何实现通信的。

通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。如下图:

image.png

这种传统的 IPC 通信方式有两个问题:

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

Binder 跨进程通信原理

理解了 Linux IPC 相关概念和通信原理,接下来我们正式介绍下 Binder IPC 的原理。

动态内核可加载模块 && 内存映射

正如前面所说,跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。

那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?难道是和前面说的传统 IPC 机制一样,先将数据从发送方进程拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方进程,通过两次拷贝来实现吗?显然不是,否则也不会有开篇所说的 Binder 在性能方面的优势了。

这就不得不通道 Linux 下的另一个概念:内存映射

Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

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

Binder IPC 实现原理

Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。

比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。

而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。

一次完整的 Binder IPC 通信过程通常是这样:

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

如下图:

image.png

Binder的优势

Android 系统是基于 Linux 内核的,Linux 已经提供了管道、消息队列、共享内存和 Socket 等 IPC 机制。那为什么 Android 还要提供 Binder 来实现 IPC 呢?主要是基于性能稳定性安全性几方面的原因。

性能

首先说说性能上的优势。Socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder 只需要一次数据拷贝,性能上仅次于共享内存。

image.png
稳定性

再说说稳定性,Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。

安全性

另一方面就是安全性。Android 作为一个开放性的平台,市场上有各类海量的应用供用户选择安装,因此安全性对于 Android 平台而言极其重要。作为用户当然不希望我们下载的 APP 偷偷读取我的通信录,上传我的隐私数据,后台偷跑流量、消耗手机电量。传统的 IPC 没有任何安全措施,完全依赖上层协议来确保。首先传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。传统的 IPC 只能由用户在数据包中填入 UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标识只有由 IPC 机制在内核中添加。其次传统的 IPC 访问接入点是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。同时 Binder 既支持实名 Binder,又支持匿名 Binder,安全性高。

基于上述原因,Android 需要建立一套新的 IPC 机制来满足系统对稳定性、传输性能和安全性方面的要求,这就是 Binder。

最后用一张表格来总结下 Binder 的优势:

image.png

三、为什么 Android 要采用 Binder 作为 IPC 机制?

这道题想考察什么?

Binder作为IPC机制的优势。

考生应该如何回答

简单来说,Binder 是android系统工程师为android 定制的一个跨进程通信方法,当然它也不是android 系统原创的,是参考了OpenBinder的实现而引进到Google的。Binder是综合了android系统的特点,从性能,设计架构,安全性等几个方面的综合平衡而设计的,具体的关于Binder的实现细节,朋友们可以参考 上面的题目 《描述下Binder机制原理》进行系统学习。

应该从几个方面与传统IPC机制做对比。

  1. 性能方面

    • 拷贝数据需要花时间,Binder只需拷贝一次,共享内存无需拷贝,其他的需要拷贝两次。
    • 从速度上来说,Binder仅次于共享内存,优于Socket,消息队列,管道,信号,信号量等。
  2. 特点方面

    • Binder:基于C/S 架构,易用性高。

    • 共享内存:

      • 多个进程共享同一块内存区域,必然需要某种同步机制。
      • 使用麻烦,容易出现数据不同步,死锁等问题。
    • Socket:

      • socket作为一款通用接口,其传输效率低,开销大。
      • 主要用在跨网络的进程间通信和本机上进程间的低速通信。
  3. 安全性方面

    • Binder:(安全性高)

      • 为每个APP分配不同UID,通过UID鉴别进程身份。
      • 即支持实名Binder,又支持匿名Binder。
    • 传统IPC:(不安全)

      • 完全依赖上层协议,只能由用户在数据包中填入UID/PID。
      • 访问接入点是开放的,任何程序都可以与其建立连接。

通过上面几个比较,特别是安全性这块,所以最终Android选择使用Binder机制进行通信。

四、Binder线程池的工作过程是什么样?

这道题想考察什么?

这道题想考察同学对于Binder线程池的理解。

考生应该如何回答

总的来说,有以下几点情况:1)binder线程池并非一个传统意义上的线程池结构,它在client进程中只有一个继承自Thread的PoolThread类。而线程的启动以及管理都是由binderDriver来控制的。2)binder线程有主线程和非主线程之分,主线程是启动的时候才会有的,每个binder线程池只有一个。其他情况下申请的都是非主线程。 3)binder线程池启动的时候,实际上只是启动了client中的binder主线程。4)binder线程(非主线程)有两种情况启动:client进程向binderDriver发送IPC请求,以及 client进程向binderDriver回复IPC请求结果。5)binder线程池的默认大小是16,1个主线程和15个非主线程。更多的细节我们就需要从Binder线程创建开始讲解了,大家可以参考下面的内容:

1.Binder线程创建

Binder线程创建与其所在进程的创建中产生,Java层进程的创建都是通过Process.start()方法,向Zygote进程发出创建进程的socket消息,Zygote收到消息后会调用Zygote.forkAndSpecialize()来fork出新进程,在新进程中会调用到RuntimeInit.nativeZygoteInit方法,该方法经过jni映射,最终会调用到app_main.cpp中的onZygoteInit,那么接下来从这个方法说起。

2.onZygoteInit

// app_main.cpp
virtual void onZygoteInit() {
    //获取ProcessState对象
    sp<ProcessState> proc = ProcessState::self();
    //启动新binder线程
    proc->startThreadPool();
}

ProcessState::self()是单例模式,主要工作是调用open()打开/dev/binder驱动设备,再利用mmap()映射内核的地址空间,将Binder驱动的fd赋值ProcessState对象中的变量mDriverFD,用于交互操作。startThreadPool()是创建一个新的binder线程,不断进行talkWithDriver()。

3.PS.startThreadPool

// ProcessState.cpp
void ProcessState::startThreadPool()
{
    AutoMutex _l(mLock);    //多线程同步
    if (!mThreadPoolStarted) {
        mThreadPoolStarted = true;
        spawnPooledThread(true);  
    }
}

启动Binder线程池后, 则设置mThreadPoolStarted=true. 通过变量mThreadPoolStarted来保证每个应用进程只允许启动一个binder线程池, 且本次创建的是binder主线程(isMain=true). 其余binder线程池中的线程都是由Binder驱动来控制创建的。

4.PS.spawnPooledThread

// ProcessState.cpp
void ProcessState::spawnPooledThread(bool isMain)
{
    if (mThreadPoolStarted) {
        //获取Binder线程名
        String8 name = makeBinderThreadName();
        //此处isMain=true
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}

4-1.makeBinderThreadName

// ProcessState.cpp
String8 ProcessState::makeBinderThreadName() {
    int32_t s = android_atomic_add(1, &mThreadPoolSeq);
    String8 name;
    name.appendFormat("Binder_%X", s);
    return name;
}

获取Binder线程名,格式为Binder_x, 其中x为整数。每个进程中的binder编码是从1开始,依次递增; 只有通过spawnPooledThread方法来创建的线程才符合这个格式,对于直接将当前线程通过joinThreadPool加入线程池的线程名则不符合这个命名规则。 另外,目前Android N中Binder命令已改为Binder:<pid>_x格式, 则对于分析问题很有帮忙,通过binder名称的pid字段可以快速定位该binder线程所属的进程p.

4-2.PoolThread.run

// ProcessState.cpp
class PoolThread : public Thread
{
public:
    PoolThread(bool isMain)
        : mIsMain(isMain)
    {
    }

protected:
    virtual bool threadLoop() {
        IPCThreadState::self()->joinThreadPool(mIsMain); 
        return false;
    }
    const bool mIsMain;
};

从函数名看起来是创建线程池,其实就只是创建一个线程,该PoolThread继承Thread类。t->run()方法最终调用 PoolThread的threadLoop()方法。

5. IPC.joinThreadPool

// IPCThreadState.cpp
void IPCThreadState::joinThreadPool(bool isMain)
{
    //创建Binder线程
    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
    set_sched_policy(mMyThreadId, SP_FOREGROUND); //设置前台调度策略

    status_t result;
    do {
        processPendingDerefs(); //清除队列的引用
        result = getAndExecuteCommand(); //处理下一条指令

        if (result < NO_ERROR && result != TIMED_OUT
                && result != -ECONNREFUSED && result != -EBADF) {
            abort();
        }

        if(result == TIMED_OUT && !isMain) {
            break; ////非主线程出现timeout则线程退出
        }
    } while (result != -ECONNREFUSED && result != -EBADF);

    mOut.writeInt32(BC_EXIT_LOOPER);  // 线程退出循环
    talkWithDriver(false); //false代表bwr数据的read_buffer为空
}
  • 对于isMain=true的情况下, command为BC_ENTER_LOOPER,代表的是Binder主线程,不会退出的线程;
  • 对于isMain=false的情况下,command为BC_REGISTER_LOOPER,表示是由binder驱动创建的线程。

6. processPendingDerefs

// IPCThreadState.cpp
void IPCThreadState::processPendingDerefs()
{
    if (mIn.dataPosition() >= mIn.dataSize()) {
        size_t numPending = mPendingWeakDerefs.size();
        if (numPending > 0) {
            for (size_t i = 0; i < numPending; i++) {
                RefBase::weakref_type* refs = mPendingWeakDerefs[i];
                refs->decWeak(mProcess.get()); //弱引用减一
            }
            mPendingWeakDerefs.clear();
        }

        numPending = mPendingStrongDerefs.size();
        if (numPending > 0) {
            for (size_t i = 0; i < numPending; i++) {
                BBinder* obj = mPendingStrongDerefs[i];
                obj->decStrong(mProcess.get()); //强引用减一
            }
            mPendingStrongDerefs.clear();
        }
    }
}

7. getAndExecuteCommand

// IPCThreadState.cpp
status_t IPCThreadState::getAndExecuteCommand()
{
    status_t result;
    int32_t cmd;

    result = talkWithDriver(); //与binder进行交互
    if (result >= NO_ERROR) {
        size_t IN = mIn.dataAvail();
        if (IN < sizeof(int32_t)) return result;
        cmd = mIn.readInt32();

        pthread_mutex_lock(&mProcess->mThreadCountLock);
        mProcess->mExecutingThreadsCount++;
        pthread_mutex_unlock(&mProcess->mThreadCountLock);

        result = executeCommand(cmd); //执行Binder响应码

        pthread_mutex_lock(&mProcess->mThreadCountLock);
        mProcess->mExecutingThreadsCount--;
        pthread_cond_broadcast(&mProcess->mThreadCountDecrement);
        pthread_mutex_unlock(&mProcess->mThreadCountLock);

        set_sched_policy(mMyThreadId, SP_FOREGROUND);
    }
    return result;
}

8. talkWithDriver

//mOut有数据,mIn还没有数据。doReceive默认值为true
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    binder_write_read bwr;
    ...
    // 当同时没有输入和输出数据则直接返回
    if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;
    ...

    do {
        //ioctl执行binder读写操作,经过syscall,进入Binder驱动。调用Binder_ioctl
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        ...
    } while (err == -EINTR);
    ...
    return err;
}

在这里调用的isMain=true,也就是向mOut例如写入的便是BC_ENTER_LOOPER. 经过talkWithDriver(), 接下来程序往哪进行呢?在文章彻底理解Android Binder通信架构详细讲解了Binder通信过程,那么从binder_thread_write()往下说BC_ENTER_LOOPER的处理过程。

8-1.binder_thread_write

// binder.c
static int binder_thread_write(struct binder_proc *proc,
            struct binder_thread *thread,
            binder_uintptr_t binder_buffer, size_t size,
            binder_size_t *consumed)
{
    uint32_t cmd;
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
    void __user *ptr = buffer + *consumed;
    void __user *end = buffer + size;
    while (ptr < end && thread->return_error == BR_OK) {
        //拷贝用户空间的cmd命令,此时为BC_ENTER_LOOPER
        if (get_user(cmd, (uint32_t __user *)ptr)) -EFAULT;
        ptr += sizeof(uint32_t);
        switch (cmd) {
          case BC_REGISTER_LOOPER:
              if (thread->looper & BINDER_LOOPER_STATE_ENTERED) {
                //出错原因:线程调用完BC_ENTER_LOOPER,不能执行该分支
                thread->looper |= BINDER_LOOPER_STATE_INVALID;

              } else if (proc->requested_threads == 0) {
                //出错原因:没有请求就创建线程
                thread->looper |= BINDER_LOOPER_STATE_INVALID;

              } else {
                proc->requested_threads--;
                proc->requested_threads_started++;
              }
              thread->looper |= BINDER_LOOPER_STATE_REGISTERED;
              break;

          case BC_ENTER_LOOPER:
              if (thread->looper & BINDER_LOOPER_STATE_REGISTERED) {
                //出错原因:线程调用完BC_REGISTER_LOOPER,不能立刻执行该分支
                thread->looper |= BINDER_LOOPER_STATE_INVALID;
              }
              //创建Binder主线程
              thread->looper |= BINDER_LOOPER_STATE_ENTERED;
              break;

          case BC_EXIT_LOOPER:
              thread->looper |= BINDER_LOOPER_STATE_EXITED;
              break;
        }
        ...
    }
    *consumed = ptr - buffer;
  }
  return 0;
}

处理完BC_ENTER_LOOPER命令后,一般情况下成功设置thread->looper |= BINDER_LOOPER_STATE_ENTERED。那么binder线程的创建是在什么时候呢? 那就当该线程有事务需要处理的时候,进入binder_thread_read()过程。

8-2.binder_thread_read

binder_thread_read(){
  ...
retry:
    //当前线程todo队列为空且transaction栈为空,则代表该线程是空闲的
    wait_for_proc_work = thread->transaction_stack == NULL &&
        list_empty(&thread->todo);

    if (thread->return_error != BR_OK && ptr < end) {
        ...
        put_user(thread->return_error, (uint32_t __user *)ptr);
        ptr += sizeof(uint32_t);
        goto done; //发生error,则直接进入done
    }

    thread->looper |= BINDER_LOOPER_STATE_WAITING;
    if (wait_for_proc_work)
        proc->ready_threads++; //可用线程个数+1
    binder_unlock(__func__);

    if (wait_for_proc_work) {
        if (non_block) {
            ...
        } else
            //当进程todo队列没有数据,则进入休眠等待状态
            ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread));
    } else {
        if (non_block) {
            ...
        } else
            //当线程todo队列没有数据,则进入休眠等待状态
            ret = wait_event_freezable(thread->wait, binder_has_thread_work(thread));
    }

    binder_lock(__func__);
    if (wait_for_proc_work)
        proc->ready_threads--; //可用线程个数-1
    thread->looper &= ~BINDER_LOOPER_STATE_WAITING;

    if (ret)
        return ret; //对于非阻塞的调用,直接返回

    while (1) {
        uint32_t cmd;
        struct binder_transaction_data tr;
        struct binder_work *w;
        struct binder_transaction *t = NULL;

        //先考虑从线程todo队列获取事务数据
        if (!list_empty(&thread->todo)) {
            w = list_first_entry(&thread->todo, struct binder_work, entry);
        //线程todo队列没有数据, 则从进程todo对获取事务数据
        } else if (!list_empty(&proc->todo) && wait_for_proc_work) {
            w = list_first_entry(&proc->todo, struct binder_work, entry);
        } else {
            ... //没有数据,则返回retry
        }

        switch (w->type) {
            case BINDER_WORK_TRANSACTION: ...  break;
            case BINDER_WORK_TRANSACTION_COMPLETE:...  break;
            case BINDER_WORK_NODE: ...    break;
            case BINDER_WORK_DEAD_BINDER:
            case BINDER_WORK_DEAD_BINDER_AND_CLEAR:
            case BINDER_WORK_CLEAR_DEATH_NOTIFICATION:
                struct binder_ref_death *death;
                uint32_t cmd;

                death = container_of(w, struct binder_ref_death, work);
                if (w->type == BINDER_WORK_CLEAR_DEATH_NOTIFICATION)
                  cmd = BR_CLEAR_DEATH_NOTIFICATION_DONE;
                else
                  cmd = BR_DEAD_BINDER;
                put_user(cmd, (uint32_t __user *)ptr;
                ptr += sizeof(uint32_t);
                put_user(death->cookie, (void * __user *)ptr);
                ptr += sizeof(void *);
                ...
                if (cmd == BR_DEAD_BINDER)
                  goto done; //Binder驱动向client端发送死亡通知,则进入done
                break;
        }

        if (!t)
            continue; //只有BINDER_WORK_TRANSACTION命令才能继续往下执行
        ...
        break;
    }

done:
    *consumed = ptr - buffer;
    //创建线程的条件
    if (proc->requested_threads + proc->ready_threads == 0 &&
        proc->requested_threads_started < proc->max_threads &&
        (thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
         BINDER_LOOPER_STATE_ENTERED))) {
        proc->requested_threads++;
        // 生成BR_SPAWN_LOOPER命令,用于创建新的线程
        put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer);
    }
    return 0;
}

当发生以下3种情况之一,便会进入done:

  • 当前线程的return_error发生error的情况;
  • 当Binder驱动向client端发送死亡通知的情况;
  • 当类型为BINDER_WORK_TRANSACTION(即收到命令是BC_TRANSACTION或BC_REPLY)的情况;

任何一个Binder线程当同时满足以下条件,则会生成用于创建新线程的BR_SPAWN_LOOPER命令:

  1. 当前进程中没有请求创建binder线程,即requested_threads = 0;
  2. 当前进程没有空闲可用的binder线程,即ready_threads = 0;(线程进入休眠状态的个数就是空闲线程数)
  3. 当前进程已启动线程个数小于最大上限(默认15);
  4. 当前线程已接收到BC_ENTER_LOOPER或者BC_REGISTER_LOOPER命令,即当前处于BINDER_LOOPER_STATE_REGISTERED或者BINDER_LOOPER_STATE_ENTERED状态。前面已设置状态为BINDER_LOOPER_STATE_ENTERED,显然这条件是满足的。

从system_server的binder线程一直的执行流: IPC.joinThreadPool –> IPC.getAndExecuteCommand() -> IPC.talkWithDriver() ,但talkWithDriver收到事务之后, 便进入IPC.executeCommand(), 接下来,从executeCommand说起.

** 9.IPC.executeCommand**

status_t IPCThreadState::executeCommand(int32_t cmd)
{
    status_t result = NO_ERROR;
    switch ((uint32_t)cmd) {
      ...
      case BR_SPAWN_LOOPER:
          //创建新的binder线程
          mProcess->spawnPooledThread(false);
          break;
      ...
    }
    return result;
}

Binder主线程的创建是在其所在进程创建的过程一起创建的,后面再创建的普通binder线程是由spawnPooledThread(false)方法所创建的。

9. 总结

Binder系统中可分为3类binder线程:

  • Binder主线程:进程创建过程会调用startThreadPool()过程中再进入spawnPooledThread(true),来创建Binder主线程。编号从1开始,也就是意味着binder主线程名为binder_1,并且主线程是不会退出的。
  • Binder普通线程:是由Binder Driver来根据是否有空闲的binder线程来决定是否创建binder线程,回调spawnPooledThread(false) ,isMain=false,该线程名格式为binder_x。
  • Binder其他线程:其他线程是指并没有调用spawnPooledThread方法,而是直接调用IPC.joinThreadPool(),将当前线程直接加入binder线程队列。例如: mediaserver和servicemanager的主线程都是binder线程,但system_server的主线程并非binder线程。

Binder的transaction有3种类型:

  1. call: 发起进程的线程不一定是在Binder线程, 大多數情況下,接收者只指向进程,并不确定会有哪个线程来处理,所以不指定线程;
  2. reply: 发起者一定是binder线程,并且接收者线程便是上次call时的发起线程(该线程不一定是binder线程,可以是任意线程)。
  3. async: 与call类型差不多,唯一不同的是async是oneway方式不需要回复,发起进程的线程不一定是在Binder线程, 接收者只指向进程,并不确定会有哪个线程来处理,所以不指定线程。

今天的面试分享到此结束拉~下期在见

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容