1. 进程和线程
- 进程:系统中正在运行的一个应用程序就是一个进程,进程是资源分配的最小单位;
- 线程:程序执行的最小单位,包含在进程中,一个进程可以包含多个线程;
2. Android应用中的多进程
2.1 dalivk虚拟机
Android 系统的底层任务管理以及驱动都是基于Linux系统。运行Java程序的时候Linux系统会启动一个Java虚拟机来运行该Java程序,Android系统是一个特殊的Linux系统,当启动一个App,系统会为该App分配一个Linux进程,而该进程中就会有一个dalivk虚拟机(DVM)实例来运行该App。
不同的App运行在不同的进程中,对应着不同的dalivk虚拟机,就对应着不同的地址空间。在一个应用内,如果新开一个进程,由于系统在新开进程的时候会分配独立的dalivk虚拟机,那么对于该App内的不同进程的实例是相互独立的。
- 每个进程都有独立的dalivk虚拟机,对应着单独的内存空间;
- 一个应用可以有多个进程,就有多个dalivk虚拟机,对应多个内存空间;
- 一个进程可以被多个应用访问,多个应用可以共享该进程。
2.2 Linux 系统组成
在Linux系统中,虚拟内存空间(运行软件程序的空间,是对物理空间的映射)只有4G,最高的1GB(对应虚拟地址0xC0000000到0xFFFFFFFF)被成为内核空间,而较低的 3GB(对应虚拟地址0x00000000到0xBFFFFFFF)被成为用户空间。内核空间是Linux内核运行空间,用户空间是应用程序的运行空间。不同进程之间,用户空间不共享,但是内核空间可以共享。
- 内核空间:可以访问受保护的内存空间,有访问底层硬件设备的所有权限;
- 用户空间:上层应用程序和Native层的运行空间,用户空间没法直接访问内核空间,需要系统调用才能访问内核空间。
2.3 自定义多进程
Android 提供了一种方式,就是在 AndroidManifest 文件中可以通过 “android:process” 来指定进程:
- 不指定 process:默认的进程,进程名为包名;
- 指定 process,但以":"开头:该进程为当前APP的私有进程,不允许其他APP访问
- 指定process,但以小写字母开头的字符串:该进程为全局进程 ,其他应用可设置相同的shareUID来共享该进程
引入多进程的场景:
- 由于 Android 系统会限制每个应用的最大内存,所以如果一个应用需要更多可用的内存时,就需要引入多进程,让某些模块运行在另外的进程中,获取更多的内存;
- 由于不同的应用运行在不同的进程中,但是如果两个不同的应用之间需要进行数据通信,需要引入多进程进行进程间通信。
3. 跨进程通信
多进程之间的通信又称IPC(Inter Process Communication),Android 中的跨进程通信的方法有以下几种:
Linux系统中常见的IPC方式:
- 管道Pipe:在内存中创建一个共享文件,利用共享文件传递信息。该共享文件并不是文件系统,只存在于内存中;只能在一个方向上流动;
- 信号Signal:异步通信。信号在用户空间和内核空间之间交互,内核可利用信号来通知用户空间的进程发生哪些系统事件。不适用于信号交换,适用于过程中断控制;
- 信号量Semaphore:控制多个进程对共享资源的访问。主要是进程间以及同一进程不同线程之间的同步手段;
- 消息队列 Message Queue:存放在内存中并由消息对了标识符标识,允许一个或多个进程对它进行读写消息。信息会复制两次,不适用于频繁或信息量大的通信;
- 共享内存Shared Memory:直接读写内核的一块内存空间。不需要进行数据拷贝;
- 套接字Socket:不同机器之间进程间通信;
Android特有场景的IPC方式:
- Binder:Binder是Android中的一种跨进程通信机制,采用服务端/客户端结构,主要包含服务端、客户端、ServiceManager、Binder驱动四大部分。
- Bundle:实现了Parcelable接口,常用于Activity、Service、BroadcastReceive之间的通信,常见常用,只能传输Bundle支持的数据类型,不适合大量数据的传输;
- File文件共享:通过文件进程多进程通信,常用于五并发,交换实时性不高的数据;
- Messenger:低并发的一对多即时通信。串行的方式处理Client发来的消息,只能传输数据,不能方法调用(RPC);
- ContentProvider:Android四大组件之一,常用于进程间数据共享,特别是一对多的情况,不过受限于AIDL;
- 匿名共享内存:在Android系统中,提供了独特的匿名共享内存子系统Ashmem(Anonymous Shared Memory),它利用了Linux的 tmpfs文件系统(一种可以基于RAM或是SWAP的高速文件系统)来实现不同进程间的内存共享。有两个特点:一是能够辅助内存管理系统来有效地管理不再使用的内存块;二是通过Binder进程间通信机制来实现进程间的内存共享;
- AIDL:Android Interface Definition Language,进程间通信的一种机制,在.aidl文件中定义AIDL接口,将其保存在承载服务的应用程序和任何其他绑定到该读物的应用程序的源代码(在src /目录中)。当应用程序构建包含.aidl文件时,Android SDK工具将生成一个基于.aidl文件的IBinder接口,并将其保存在项目的gen /目录中。 该服务必须适当地实现IBinder接口。 然后,客户端应用程序可以绑定到服务并从IBinder调用方法来执行IPC。
- Socket:允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据;
除去 Socket,其他的都是基于 Binder 机制实现的。
3. 传统的跨进程通信与Binder
Android 系统基于 Linux 系统,因此Linux系统的IPC方法:共享内存、消息队列、管道、信号量,在Android系统都可以使用。以32位系统来说,在每一个Linux进程都有3G虚拟内存空间,称之为用户空间,以及1G的内核空间。其中,不同进程的用户空间是相互隔离不可直接通讯的,但是内核空间却是共享的。以进程A发送数据到进程B为例,需要经历如下步骤:
- 进程A的内核空间开辟一块内核缓存区,通过copy_from_user()系统调用,将在用户空间的数据拷贝到内核缓存区;
- 进程B的内核空间也开辟了一块内核缓存区,因为不同进程的内核空间共享的,此时进程B就可以通过copy_to_user()系统调用将内核缓存区的数据拷贝到自己的用户空间;
如此就完成了一次进程间通讯,上面说到的管道、消息队列都是基于这种方式,因此有两个缺点:
- 经历2次数据拷贝,效率较低;
- 进程B接收的内核缓存区无法确定开辟多大的空间合适,太大自然损耗空间,太小则可能出现拷贝不完整,因此要么开辟尽可能大的空间,要么先获取进程A发送的数据大小再开辟对应大小的空间。这两种做法要么浪费空间,要么浪费时间。
使用Binder的理由可以总结为性能、安全、稳定几个方面:
- 性能:管道、消息队列、Socket都需要2次数据拷贝(即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区),而Binder只要1次,且Binder相对Socket方法也更加高效,Socket主要用于跨网络的进程间通讯,传输的效率比较低。当然共享内存1次数据拷贝都不需要,因此就性能而言,共享内存高于Binder;
- 安全:Android系统为每个应用都分配各自的UID以鉴别不同进程。Binder会验证权限,鉴定UID/PID来验证身份,保证了进程通信的安全性;
- 稳定:共享内存1次数据拷贝都不需要,但实现的方法比较复杂,并且需要做好进程同步,容易出现死锁或者资源竞争,稳定性差。而Binder基于C/S架构,客户端和服务端彼此独立,稳定性强。
因此,在Android系统的IPC方式,一般情况下都推荐使用Binder。当然,在Android源码里,上述其他IPC方法也会使用到。
4. Binder 通信原理
了解Binder通信原理需要知道两个概念:
- Binder驱动:Linux有一个叫“动态内核可加载模块”的机制,因此Android系统就可以通过动态添加这个模块,让该模块链接到内核作为内核的一部分,实现用户进程通过这个内核模块作为通讯桥梁。这个模块就是“Binder驱动”。
- 内存映射:内存映射即经典的mmap()方法,该方法可以将进程的用户空间的一块用户内存区域A映射到内核空间B,映射后,用户内存区域A的修改都会直接反应到内核空间B,反之亦然。
知道这两个概念后,以进程A发送数据到进程B为例,一次完整的Binder通信的步骤如下:
- Binder驱动链接到内核空间后,开辟出一个数据接收缓存区1;
- 进程A在内核空间开辟一块内核缓存区2,接着将数据接收缓存区1和进程A的内核缓存区2做内存映射,同时再将数据接收缓存区1和进程B的用户空间中的用户内存区域3也做内存映射。如此一来,进程A的内核缓存区2的修改也会同步反映到程B的用户空间中的用户内存区域3中;
- 进程A通过copy_from_user()系统调用,将在用户空间的数据拷贝到内核缓存区2中,此时这些数据就会同步反映到B的用户空间中的用户内存区域3中。
这样便完成了一次进程间通讯,并只有1次数据拷贝。
5. Binder通信模型
5.1 服务端、客户端、ServiceManager、Binder驱动
文章一开头就说过,Binder基于服务端/客户端架构,主要包括服务端、客户端、ServiceManager、Binder驱动这4个部分。以进程A与进程B通讯为例,进程B就是客户端,进程A为服务端。其中服务端、客户端、ServiceManager运行在用户空间,只有Binder驱动运行在内核空间。ServiceManager和Binder驱动是系统提供的,服务端和客户端则由应用程序来实现。
5.1.1 Binder驱动
Binder驱动在系统里对应/dev/binder文件,负责进程间Binder通讯的建立。是服务端、客户端、ServiceManager这3者之间的桥梁。Binder跨进程传递、Binder引用计数管理、数据包跨进程传递等一系列操作都需要Binder提供支持。
5.1.2 ServiceManager
ServiceManager作用很强大,在Binder通讯中,主要作用管理服务端和客户端。系统中会有一个进程使用BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager。此时,Binder驱动会为该ServiceManager进程在内核空间创建对应的Binder实体,这个实体可以在其他的进程通过第0号Binder引用获得。那么,其他进程都可以通过第0号引用和ServiceManager的Binder通讯有什么作用呢?
5.1.3 服务端向ServiceManager注册
该案例中,进程B作为服务端,可以通过第0号引用获得ServiceManager的Binder引用,通过该Binder引用和ServiceManager通讯。进程B会先创建自己的Binder实体,并起个容易记得的名字,如“进程B”,将这个Binder实体和名字一起打包,通过第0号引用通知ServiceManager注册一个名为“进程B”的Binder。此时,会由Binder驱动为进程B创建位于内核空间的Binder实体,以及ServiceManager对该Binder实体的引用,ServiceManager可以通过“进程B”这个名字找到新创建的进程B的Binder实体。
有一个叫法问题需要统一下,在进程A和进程B通讯的案例中,进程B作为服务端。但是在进程B通过第0号引用向ServiceManager注册时,进程B其实是作为“客户端”,ServiceManager是“服务端”。也就是一个进程,比如进程B可能是提供服务的服务端,但至少对于ServiceManager这个进程来说,进程B仍然是一个“客户端“。
5.1.4 客户端获取服务端Binder引用
作为服务端的进程B已经向ServiceManager注册好,此时作为客户端的进程A也可以通过第0号引用,向ServiceManager请求访问名字叫做“进程B”的Binder引用。ServiceManager通过客户端发送过来的名字,查到对应的进程B的Binder引用回复给进程A。进程A就可以通过该Binder引用和进程B通讯了。
到此为止,Binder驱动为作为服务端的进程B创建了位于内核空间的Binder实体,同时ServiceManager拥有该Binder实体的引用,作为客户端的进程A也拥有该Binder实体的引用。如果接下来有进程C也作为客户端请求进程B的Binder,则最后进程C也会拥有进程B的Binder实体的引用。
5.2 Binder通讯中的代理模式
上面说到作为客户端的进程A通过Binder驱动获取到了作为服务端的进程B的Binder引用,这个Binder引用更准确来说应该称为BinderProxy,也就是进程A获取到的是进程B的Binder代理对象,通过该Binder代理对象,进程A可以调用进程B提供的方法。
当然,因为进程A和B毕竟存在进程边界。所以进程A获取到的进程B的Binder代理对象并非可以直接使用进程B提供的方法。进程A只是调用方法时,把请求参数打包成Parcel对象,通过Binder代理对象发给Binder驱动,Binder驱动看到这个Binder代理对象是进程B的Binder代理,就会通知进程B解包Parcel对象,使用传入的请求参数,调用对应的方法,最后把进程B执行完的结果返回给进程A。但对于进程A来说,就像是直接调用了进程B的方法。
5.3 好多Binder分不清?
在上面说的服务端、客户端、ServiceManager、Binder驱动这4个部分,他们的代表都是“Binder”,Binder一词出现多次,可能有部分同学觉得有点乱,现在统一理一遍:
- 通常意义:通常我们说“Binder”,就是指Android里面的一种跨进程通讯机制;
- 服务端进程:对服务端进程来说,“Binder”指服务端进程创建的自己的Binder本地对象;
- 客户单进程:对客户端进程来说,“Binder”指客户端进程从ServiceManager拿到的服务端进程里的Binder本地对象,在客户端进程的一个Binder代理对象;
- 传输过程:在跨进程传输过程中,“Binder”是可以进行跨进程传递的对象;Binder驱动会对具有跨进程传递能力的对象做特殊处理,会自动完成代理对象和本地对象的转换。
5.4 Binder通讯流程
综上,服务端、客户端、ServiceManager、Binder驱动之间的通讯过程大致如下:
- 系统会有一个进程使用BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager;
- 欲提供服务的进程作为服务端进程,先创建自己的Binder实体,再向ServiceManager注册该Binder实体,表明可以对外提供服务。Binder驱动会为服务端进程的Binder创建位于内核空间的Binder实体,同时,ServiceManager会拥有该内核空间的Binder实体的引用;
-
客户端通过名字在Binder驱动的帮助下,从ServiceManager获取到服务端内核空间的Binder实体的引用,即服务端的Binder代理,通过这个代理就可以实现和服务端的通讯。
看一个跨进程调用系统服务的demo:
//获取WindowManager服务引用
WindowManager wm = (WindowManager) getSystemService(getApplication().WINDOW_SERVICE); //1
//布局参数layoutParams相关设置略...
View view = LayoutInflater.from(getApplication()).inflate(R.layout.float_layout, null);
//添加view
wm.addView(view, layoutParams);
系统开机流程中,会提前注册一系列系统服务,如WindowManager,一个用于管理窗口的系统服务。我们在通过传入名字,从ServiceManager获取到WindowManager这个服务的本地代理,也就是wm变量,通过wm代理就可以远程调用WindowManager服务进程里提供的addView()方法。
参考资源:
Android Binder -李青涛
一文读懂 Android 跨进程通信机制
Android Binder浅析