多进程的好处
先看一下市面上主流APP的多进程情况,以高德地图和百度地图为例:
会发现这两款APP都是有多个进程的,而且IPC也属于Android中比较高阶的领域,所以多少还是了解一下。
多进程在Android领域并不是一个新鲜的词汇,Android为多进程提供了多种通信机制,多进程一般有如下优点:
- 内存:占用内存越大的进程,通常被系统杀死的可能性越大。让一个组件运行在单独的进程中,可以减少主进程所占用的内存,降低被系统杀死的概率.
- 崩溃率:如果子进程因为某种原因崩溃了,不会直接导致主程序的崩溃,可以降低我们程序的崩溃率。
- 独立:即使主进程退出了,我们的子进程仍然可以继续工作,假设子进程是推送服务,在主进程退出的情况下,仍然能够保证用户可以收到推送消息
知识点回顾
内核空间和用户空间
对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
对上面这段内容我们可以这样理解:
每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。
换句话说就是, 最高 1G 的内核空间是被所有进程共享的!
所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。
比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 "<u>系统调用</u>" 告诉内核:"我要读取磁盘上的某某文件"。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。
简单说就是应用程序把高科技的事情(从磁盘读取文件)外包给了系统内核,系统内核做这些事情既专业又高效。
Android进程
默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。
各类组件元素的清单文件条目—<activity>
、<service>
、<receiver>
和 <provider>
—均支持 android:process
属性,此属性可以指定该组件应在哪个进程运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。
内存映射文件
先看两个生活中非常常见的生活场景:
-
火车站窗口排队买票:
每次我排队买票我都担心排错队伍,感觉很不好。
-
银行窗口办业务:
相比较来说,银行办业务排队就舒服很多。这里面有个系统对客户的号和窗口做了个映射。
MMAP(内存映射)
前面讲了,进程读取内存的流程为:
将文件内容从硬盘拷贝到内核空间的一个缓冲区,如图中过程1,然后再将这些数据拷贝到用户空间,如图中过程2,在这个过程中,实际上完成了两次数据拷贝
“映射”这个词,就和数学课上说的“一一映射”是一个意思,就是建立一种一一对应关系,在这里主要是只 硬盘上文件 的位置与进程 逻辑地址空间 中一块大小相同的区域之间的一一对应,如图1中过程1所示。这种对应关系纯属是逻辑上的概念,物理上是不存在的,原因是进程的逻辑地址空间本身就是不存在的。在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。
Binder
Android中的binder便是基于内存映射的原理,通过一遍拷贝,在效率和安全两个维度成为最佳选择:
下面细分析一下通信过程中涉及到的角色:
因为binder是一个驱动,具备进程间通信的能力,所以binder会在调用进程和服务进程各提供一个角色,用来进行通信,分别是Proxy和Stub,调用进程和服务进程分别通过Proxy和Stub进行通信。
进程间通信的本质原理我们了解了,就是数据的拷贝,多以对于数据应该有个约束,就是Parcelable,所以Android中进行进程间通信有这三个角色就好了。
例子
定义AIDL接口,build工程会生成对应代码:
红圈标注的就是对应的角色。
其实这个转换过程很简单,每天都在用,当你在IDE里面新建一个类文件的时候,会自动生成一个类,类名字都已经写好了,这个就是抽象语法树完成的,感兴趣可以去看下JDT。
接下来就是调用进程如何获取Proxy了,通过bindService方式:
通过binder的asInterface方法获取到Proxy,从而进行通信,而且BindService提高了被绑定服务的优先级,避免服务进程轻易被系统回收。