本文将介绍 Android 实现进程间通信的几种方式,进入正题之前,我们首先来了解下多进程的概念,知道多进程是什么,以及为什么要用多进程。
1 多进程
1、一般情况下,一个应用程序就是一个进程,这个进程名称就是应用程序包名。每个进程都有自己独立的资源和内存空间,别的进程是不能任意访问其他进程的内存和资源。
2、单进程的局限:每个进程所能使用的资源是有限,特别是内存,安卓系统对用户进程有严格的内存要求,超过此内存限制时,应用将OOM和崩溃。所以,Android引入了多进程的概念,它允许在同一个应用内,为了分担主进程的压力,将占用内存的某些页面单独开一个进程。
3、Android多进程创建很简单,只需要在AndroidManifest.xml的声明四大组件的标签中增加”android:process”属性即可。命名之后,就成了一个单独的进程。”android:process”属性以【:】开头为当前应用的私有进程(:remote),否则为全局进程(如:com.xxx.xxx)。
4、多进程应用运行时application的onCreate()会执行多次,所以使用的时候就要根据进程名判断当前所处的进程来进行对应的初始化操作。
5、静态成员和单例失效:每个进程保持各自的静态成员和单例,相互独立。
6、Android会为每一个进程分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间。它们要访问对方的数据的时候,都要通过进程间通信的方式来进行。
2 进程间通信方式
2.1 Intent/Bundle
Activity,Service,Receiver 都支持在 Intent 中传递 Bundle 数据,而 Bundle 实现了 Parcelable 接口,可以在不同的进程间进行传输。所以,在一个进程中启动了另一个进程的 Activity,Service 和 Receiver ,可以在 Bundle 中附加要传递的数据通过 Intent 发送出去。
提示
Intent 不仅可以在进程间传输 Bundle(Parcelable)类型数据、基本类型数据,对于 Serializable 类型的数据也支持在进程间传递。但是我们一般不用 Serializable 作为进程间数据传递的载体,至于为什么,请参考这篇文章的序列化部分。文中提到的 Serializable 序列化、反序列化方式是反射,以反序列化为例,源码追溯路径是 readObject-> readObject0 -> readOrdinaryObject -> invokeReadResolve>readResolveMethod.invoke(…)。
2.2 文件共享
1、可以在进程中序列化数据对象到文件中,然后在另一个进程中反序列化这个对象。
2、SharedPreferences 由于系统对SP的读写有一定的缓存策略,使内存中有一份SP文件,导致系统对它的读写不可靠。当高频读写操作是,SP会有数据丢失的风险,所以IPC不建议采用这种方式。
3、SQLite 支持多进程并发访问。请参考这篇文章,笔者没有实践过。
4、使用 MMKV 库(后续文章再具体介绍)。
2.3 Messager
通过Messenger可以在不同进程之间传递Message对象,Message中可以放入我们需要传递的数据,它的底层实现是AIDL。
但是Messenger在服务端的Handler是以串行的方式处理来自客户端的Message,所以如果有大量的并发请求,Messenger就效率低下,所以Messenger适用于数据并发量低的进程间通信。
具体使用请参考官方文章>>
下面以一个音频播放器的例子讲解 Messager 的实际使用,关于音乐类软件开发思路笔者这篇文章中有列举参考资料)。
首先,我们从 MediaBrowserCompat 的 connect 方法说起。
1 | public void connect() { |
方法中的 mImpl 是我们在构造 MediaBrowser 对象时创建的。
1 | public MediaBrowserCompat(Context context, ComponentName serviceComponent, |
以上 mImpl 的四种实现中只有 MediaBrowserImplBase 是以 Messenger 方式进行的(其它三个是 AIDL 的方式),下面我只选取 MediaBrowserImplBase 这个实现方式加以说明。
于是由mImpl.connect()走到了 MediaBrowserImplBase 的 connect 方法。
1 | public void connect() { |
只看主要部分,可见 MediaBrowserCompat 的 connect 方法最终做的工作就是调用 bindService 方法绑定了服务,其中 mServiceConnection 是一个MediaServiceConnection对象,他是MediaBrowserImplBase 的内部类,下面我们看看MediaServiceConnection 这个类做了什么。
1 | private class MediaServiceConnection implements ServiceConnection { |
我们重点关注 onServiceConnected 方法的实现,看代码注释,这里面做了两个重要的事,一个是由服务端返回的 binder(BinderProxy) 构造出 ServiceBinderWrapper 对象,之后在 ServiceBinderWrapper 构造方法内得到了一个 Messenger 对象,这个 messager 用于向服务端发送消息。第二个是调用 ServiceBinderWrapper 的 connect 方法将客户端构造的 Messenger 对象 mCallbacksMessenger 发送到了服务端,这样服务端就可以向这个客户端发送消息了。最终实现了由服务端到客户端,客户端到服务端的双向沟通机制。下面是 ServiceBinderWrapper 的构成。
1 | private static class ServiceBinderWrapper { |
大体流程,首先是在构造函数中根据服务端返回的 binder 创建一个 Messenger 对象,下面的方法都最终调用了 sendRequest 方法,而 sendRequest 方法最后又调用了 Messager 的 send 方法往服务端发送消息,同时也会带上本地的 Messenger 对象 mCallbacksMessenger。
以上就是客户端的流程,下面进入服务端的流程。
当客户端调用 bindService 方法后,对于服务端来说最终会调用到它的 onCreate 方法。
1 | public void onCreate() { |
我们只关注最后一种情况,即调用 MediaBrowserServiceImplBase 的 onCreate 方法(其它 3 个是 AIDL 方式 )。
1 | class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { |
MediaBrowserServiceImplBase 中,首先在 onCreate 中创建了类型为 Messenger 的 mMessenger 对象,之后在 onBind 方法中通过 mMessenger.getBinder() 返回 IBinder 供客户端使用,客户端拿到这个 IBinder 实例之后就可以往服务器发送消息了,之后的流程就是之前提到的客户端逻辑了。
通过解读源码,大家可以发现 Compat 包中对于不同 API 等级,使用的进程间通信方式是不一样的,以上只是一个很小的切入点,其它的方面还需自行阅读源码。总之一个原则就是,不管是 Messenger 还是 AIDL ,要实现进程间的双向沟通,就必须都拿到对方的 IBinder 代理对象。
2.4 AIDL
AIDL 可以解决并发和跨进程调用方法的问题,本质是利用了 Binder 传输机制,其实 Messenger 本质上也是 Binder ,只不过Messager不适合处理大量并发的消息处理,也不支持跨进程调用方法。
通过编写aidl文件来设计想要暴露的接口,编译后会自动生成对应的java文件,服务器将接口的具体实现写在Stub中,用 IBinder 对象传递给客户端,客户端 bindService 的时候,用 asInterface 的形式将 iBinder 还原成接口,再调用其中的方法。
注意:AIDL 支持单独的基本类型变量以及 String,CharSequence 类型变量的传递。他们也可以作为 Parcelable 子类的一个属性存在。Serializable 不支持 AIDL 传递,但可以放置在 Bundle 中传递。除了基本类型外,其它的自定义Parcelable 类型必须在单独的aidl文件中导入。
除了使用 AIDL 工具实现以上的机制,我们还可以通过手写 AIDL 生成的各个类,实现进程间通信的需求。参考链接>>
AIDL进阶参考这篇文章>>
2.5 ContentProvider
系统四大组件之一,底层也是Binder实现,主要用来为其他APP提供数据,可以说天生就是为进程通信而生的。使用教程>>
2.6 Socket
Socket 是连接应用层与传输层之间的接口,常用的 Socket 类型有两种:流式 Socket(SOCK_STREAM)和数据报式 Socket(SOCK_DGRAM)。流式是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket ,对应于无连接的 UDP 服务应用。一般我们使用流式的 Socket。关于 Socket 的介绍将在后续的篇幅中讲解。
以上内容参照了下面两篇文章的部分内容模块:
1、安卓进程间通信及 App 保活的“多进程”和“进程间通信IPC方式”两大模块。
2、使用AIDL来进行进程间通信的“Android进程间通信的方式”和“进程间通信的准备”两大部分。
3 疑惑解答
3.1 Android 为什么要选择 Binder 机制来实现进程间通信
| Binder | 共享内存 | Socket | |
|---|---|---|---|
| 性能 | 数据拷贝一次 | 无需拷贝 | 数据拷贝两次 |
| 稳定 | C/S架构,清晰明朗,稳定性好。 | 需要自行处理同步等问题。 | C/S架构,传输效率低,开销大。 |
| 安全性 | 内核添加身份标识,可靠。 | 依赖上层协议。访问接入点开放,不安全。 | 依赖上层协议。访问接入点开放,不安全。 |
Binder “数据拷贝一次”是什么意思?其实这里就涉及到 mmap(memory mapping)的使用了。
虚拟内存被用户系统分为两部分:用户空间和内核空间,用户空间是用户代码运行的地方,内核空间是内核代码运行的地方,内核空间由所有进程共享。为了安全,用户空间和内核空间是隔离的,即使用户空间的程序发生崩溃,内核也不会受到影响。每个用户空间(进程)之前也是互相隔离的,为了让进程之间可以互相通信,我们可以借助共享内核来实现数据传输。
当数据从用户内存中拷贝到内核空间时,会在内核区域开辟一块虚拟内存,即 Binder 缓冲区,之后利用 mmap 技术将这块虚拟内存与接收方的虚拟内存映射到同一块物理内存上,这样就省去了从内核空间到用户空间的拷贝操作,从而实现了 binder 的一次拷贝技术(从用户空间到内核空间)。同时,binder 缓冲区的空间是有限的,所以需要及时释放内存,为后续拷贝操作做准备。
mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。下面我们利用 mmap 来实现文件写入功能。NDK使用知识请参考笔者写的两篇文章:文章一,文章二。
1 | extern "C" |
由上图可知,利用 mmap 操作文件是没有内核来参与的,发生零次数据拷贝。
而对于普通IO操作,则需要内核的参与,总共发生了两次数据拷贝。
这个时候就有疑惑了,前面讲到 binder 机制的实现利用到了 mmap 技术,可是却也经过了内核的拷贝操作啊。其实产生这个疑惑的原因是将 mmap 将内核挂钩了,这样想是不对的。我们可以把 binder 中提到的物理地址想象成文件地址,这样我们在内核区域将一段虚拟内存通过 mmap 映射到物理地址,再将用户空间的一段虚拟内存同样通过 mmap 映射到同一块物理地址后,再对内核区域虚拟空间的操作就等同于对用户空间的操作。
具体关于mmap的介绍请参考这篇文章范例模块之前的内容,当然我还有一些地方不明白的,比如“匿名内存映射”,等到具体使用时再来复盘,这里只需知道什么是 mmap,以及它的基本用法。
3.2 Intent 传递的数据大小限制
我们深入到源码中,在 system/libhwbinder/ProcessState.cpp 这个类下有这么一行定义
1 | #define DEFAULT_BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) |
可以看到进程为 Binder 分配的内存空间大小是1M-8k,而我们知道 Intent 是通过 binder 传递数据的,所以它的最大传输数据大小也不能超过 1M-8k(实际可传的数据大小会略小于这个数,因为 Intent 传输数据时会携带其它数据)。另外,Binder 是可以异步传输的,异步传输数据最大是(1M-8K)/2,除以 2 更多的是想对异步传输做一个限制,不希望异步传输消耗掉所有的 buffer。
Binder Buffer 为什么会是(1M - 8K),而不是整数值 1M ?这里看起来很奇怪,8K空间到底时用来做什么的?从GIT提交的记录可以看到下面的commits。
Modify the binder to request 1M - 2 pages instead of 1M. The backing store in the kernel requires a guard page, so 1M allocations fragment memory very badly. Subtracting a couple of pages so that they fit in a power of two allows the kernel to make more efficient use of its virtual address space.
这段解释还没有充分理解,其大致的意思是:kernel的“backing store”需要一个保护页,这使得1M用来分配碎片内存时变得很差,所以这里减去两页来提高效率,因为减去 1 页就变成了奇数。至于保护页如何影响内存分配,暂时不太清楚了。
对于Intent为什么不能传输大数据,可以再看下这篇文章。但该文章并未解释 Intent 为什么不能传1M-8k的数据,还有是内核空间是共用的,即所有进程共用 1 个内核空间,所以文中的 B 内核空间表述有误,应该改为共享内核空间。至于为什么不能传 1M-8k 的数据,笔者认为除了 Intent 除了会带有附加数据外,传递的时不只是有这个任务在使用这个 1M-8k 的 buffer(即虚拟内存),其它的任务也可能正在使用这个空间,所以实际传输能使用的空间大小会远小于这个 1M-8k ,笔者在模拟上测得大约是 505 k。
当使用 AIDL 同步方式传输数据时,可以测得最大传输值大小接近 1M-8K ,采用 AIDL 异步方式传输数据时,可以测得最大传输值大小接近(1M-8k)/2 。