上一篇文章介绍了Android中IPC的基本原理,这一篇文章详细介绍Android中各种跨进程通信的方式。
IPC的方式
具体方式有很多,但是,它们的使用场景有很大区别,下面对这些方式进行详细分析。
使用Intent
Android中的四大组件有三大组件(Activity, Service, Receiver)都支持在Intent中传递Bundle数据。由于Bundle实现了Parcelable接口,所以,它能够在不同进程中进行传输。因此,当在一个进程中启动了另一个进程的Activity、Service和Receiver组件时,可以在Bundle中附加数据,并通过Intent发送给目标进程的组件,这样就实现了跨进程通信。
不过,在传输的过程中,所传输的数据必须支持序列化。比如基本数据类型,字符串,Parcelable的实现类和Serializable的实现类。
使用文件共享
文件共享式一种比较方便的跨进程通信方式,其原理是两个进程通过read/write同一个文件来交换数据。write文件的时候将对象序列化后保存到文件中,read文件的时候通过反序列化从文件中读取数据。
这种IPC方式对文件的格式没有具体要求,可以是txt、xml或者Json,只要是读写双方约定的格式即可。
不过这种方式存在并发read/write的问题,因此,使用这种方式时要尽量避免并发read/write文件的情况,或者使用线程同步的方式控制多个线程并发read/write文件。
使用Messenger
可以使用Messenger创建跨进程通信的接口,这种方式需要使用Handler响应不同的Message对象(这里涉及到Android消息机制),因此,Handler是使用Messenger的基础。Messenger能够让服务端和客户端共享Binder对象,这样,客户端可以使用Messenger向服务端发送Message消息。另外,客户端也能定义一个Messenger,让服务端能够回传Message消息。
Messenger对AIDL做了封装,使用相对比较简单,不过,它是在一个线程中串行处理客户端请求,不需要考虑线程同步的问题。
以下是使用Messenger的主要步骤:
- 在服务端实现一个Handler,用来处理客户端发送的消息;
- 在服务端使用步骤1中的Handler初始化Messenger对象;
- 在服务端的onBind()方法中,使用步骤2中的Messenger对象创建一个Binder对象,并返回给客户端;
- 在客户端中使用步骤3中的Binder对象初始化一个Messenger对象,它用来给服务端发送Message对象;
- 服务端接在Handler中接收到Message对象后,进行相应的处理;
在服务端创建Messenger接口的代码如下所示:
1 | public class MessengerService extends Service { |
在客户端使用Binder对象创建Messenger对象,并发送消息的示例代码如下所示:
1 | public class ActivityMessenger extends Activity { |
上面的示例没有展示服务端如何回复客户端。不过,如果服务端需要回复客户端,有一点非常关键,那就是当客户端发送消息的时候,需要把接收服务器端回复的Messenger通过Message的replyTo参数传递给服务端,作为通信的桥梁。
通过Messenger进行IPC的原理图如下所示:

使用AIDL
Messenger串行处理客户端发送的消息,如果大量的消息同时发送到服务端,Messenger方式就不太合适了;同时,Messenger的主要作用是传递消息,如果要跨进程调用服务端方法,Messenger也无法做到。不过,AIDL可以实现这样的需求。
AIDL是一种接口定义语言,用于约束两个进程间的通讯规则,在进行IPC时,通信信息会被转换为AIDL协议消息,然后发送给对方,对方接收到AIDL协议消息后,再转换成相应的对象。
AIDL是由Binder机制实现的,Binder机制以在上一篇文章中介绍了,下面阐述如何使用AIDL进行IPC。
创建aidl文件
可以使用Java语法定义aidl文件,每个文件必须定义一个interface,并将interface只能有一个,然后在interface声明调用的方法。
AIDL支持以下数据类型:
- 所有Java支持的原子数据类型(比如int, long, char, boolean等)
- String
- CharSequence
- List:只支持ArrayList,里面的每个元素都必须能被AIDL支持
- Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value
- Parcelable:所有实现了Parcelable接口的对象
- AIDL:所有AIDL接口本身也可以在AIDL文件中使用
在定义AIDL接口时,需要注意以下几点:
- 如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的aidl文件,并在其中声明为parcelable类型。
- 自定义的Parcelable对象和AIDL对象必须在aidl文件中要显示import,即使他们和当前的aidl文件定义在同一个包内。
- AIDL除了原子数据类型外,其它类型的参数必须标明数据流动的方向:in、out或者inout,原子类型默认方向是in,并且不能被改变。
- 要根据具体情况标明参数的数据流动方向,不能一概使用inout,因为这在底层是有开销的。
- AIDL接口只支持声明方法,不支持声明static常量,这一点与Java中的接口不一样。
下面是一个定义AIDL接口的示例:
1 | // IRemoteService.aidl |
实现接口
创建完AIDL接口后,Build项目,这时会根据aidl文件生成一个对应Java接口文件。这个接口包含了一个名为Stub的子类,它是一个Binder接口,服务端需要实现这个接口提供对应的服务。
1 | private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { |
在实现AIDL接口定义的方法时,需要注意:
- 要考虑多线程问题;
- 默认情况下,客户端调用服务端方法(RPC)是同步的,如果客户端的UI线程进行RPC,并且这个RPC比较耗时,会造成客户端ANR。所以,在需要的时候,在工作线程中进行RPC;
- 进行RPC时抛出的Exception不会被发送到客户端;
将接口暴露给客户端
服务端实现AIDL接口后,需要将接口暴露给客户端。一般是通过Service组件,在其onBind()方法中将实现的Binder接口返回给客户端,示例代码如下:
1 | public class RemoteService extends Service { |
如果客户端和服务端属于不同的项目,需要将服务端的aidl文件复制到客户端,这时客户端才能声明AIDL定义的接口,并进行使用。
当客户端调用bindService()方法连接服务端时,onServiceConnected()回调方法会接收到服务端返回的Binder接口,将其转换为AIDL定义接口后,就可以调用服务端对应的方法了,示例代码如下所示:
1 | IRemoteService mIRemoteService; |
AIDL的进阶使用
跨进程监听器
通常会有这样的需求:客户端需要使用监听器去监听服务端数据的变化,然后做相应的处理。一般来说,会这样实现这个需求:客户端向服务端注册一个监听器,注册成功后,服务器在数据变化时,回调监听器里面的方法通知客户端数据发生变化;当客户端不需要数据监听数据变化时,就会向服务器注销这个监听器。
在AIDL中,需要使用AIDL接口定义监听器,然后,在服务器的AIDL接口中定义registerListener()和unRegisterListener()方法。由于要支持多线程,需要在服务端定义一个List去管理各个客户端的Listener,一般会想到使用CopyOnWriteArrayList去存储Listener,因为它支持多线程。不过,如果这样实现,我们可以发现:客户端可以正常register监听器,但是,客户端unRegister同一个监听器的时候,却总是失败!
之所以会出现这个情况,是因为在客户端中,虽然register和unRegister的是同一个Listener对象,但是,Listener对象的数据通过Binder机制跨进程传输到服务端,进行反序列化后,在服务端却是不同的对象。总的来说,对象是不能跨进程直接传输的。
为了解决这个问题,就需要使用RemoteCallbackList,它是系统专门提供的用于删除跨进程Listener对象的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,所以,可以使用它来代替CopyOnWriteArrayList管理客户端的Listener对象,实现跨进程监听器。
RemoteCallbackList的内部有一个Map结构专门用来保存所有的AIDL回调,其key是IBinder类型,value是Callback类型。其Map结构的具体声明如下所示:
1 | ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>(); |
其中Callback中封装了正真的远程Listener。当客户端register时,它会把Listener的信息存入到Map结构中,key和value的获取过程如下所示:
1 | IBinder key = listener.asBinder(); |
可以看到,Map结构的key值是Listener对应的Binder对象。之所以会这样,是因为Listener对象虽然是跨进程的,在客户端和服务端会是不同的对象,不过,这个跨进程的Listener在底层对应的Binder对象是同一个。正是利用这个特性,RemoteCallbackList可以正确删除掉对应的Listener对象。
同时,RemoteCallbackList内部自动实现了线程同步的功能,并且,在客户端进程意外终止后,也能够自动删除客户端注册的Listener对象。
RemoteCallbackList常用的API有:
- register和unregister:用来注册和注销客户端对应的Listener对象;
- beginBroadcast和finishBroadcast:开始使用和结束使用RemoteCallbackList对象,必须成对使用;
- getBroadcastItem(int pos):根据pos获取对应的Listener对象;
多线程问题
客户端调用服务端的远程方法时,远程方法运行在服务端的Binder线程池中,同时,客户端线程会被阻塞,如果这个线程是UI线程,并且远程方法执行比较耗时,会造成客户端出现ANR,所以,不要在UI线程中调用耗时的远程方法。
服务端的远程方法(通过实现Binder接口的方法)本身就运行在服务端的Binder线程池中,所以,服务端的远程方法本身就可以执行大量耗时操作,切记不要在远程方法中开启线程执行异步任务,除非明确知道开启异步线程的目的。
当服务端调用客户端Listener中的方法时,该方法也运行在Binder线程池中,不过是客户端的线程池,所以,相对于服务端,Listener中的方法也是远程方法,因此,也不能在服务端(要注意Service组件运行在UI线程)调用客户端的耗时远程方法。所以,要确保客户端的耗时远程方法运行在服务端的非UI线程,否则,会导致服务端无法及时响应。
客户端的远程方法运行在客户端的Binder线程池中,因此,不能在其远程方法中修改UI。如果要修改UI,需要使用Handler切换到UI线程。
服务端的Binder连接有可能意外断开,为了程序的健壮性,客户端可以在断开时,重新连接服务端。有两种方法实现这个功能:第一种是客户端连接服务端成功后,使用Binder.linkToDeath()方法设置DeathRecipient监听器,这样,在Binder断开时,客户端会收到binderDied()回调方法;第二种是在客户端连接服务端时,重载onServiceDisconnected()方法。这两种方法的区别是,binderDied()回调方法运行在客户端的Binder线程池中,而onServiceDisconnected()方法运行在UI线程中。
权限控制
默认情况下,远程服务任何客户端都能连接,这是不安全的,可以给服务加入权限验证功能。通常有两种方法:
- 在onBind()方法中验证
可以在服务端的onBind()方法中进行权限验证。下面介绍在onBind()方法中使用Permission验证:
1.在Manifest文件中声明所需的权限
1 | <permission |
2.在onBind()方法中做权限验证
1 | public onBind(Intent intent) { |
如果权限不通过,就直接返回null,那么客户端就无法访问这个服务端。
3.如果客户端需要绑定声明权限的服务端,需要在Manifest中声明权限
1 | <uses-permission android:name="com.example.ipc.permission.ACCESS_DADA" /> |
这种方式也可以用于Messenger中。
- 在onTransact()方法中验证
可以在服务端实现Binder接口时,重载onTransact()方法进行权限验证。下面介绍在onTransact()方法中获取客户端的Uid验证包名:
1 | public boolean onTransact(int code, Parcel data, Parcel reply, int flags) |
如果验证失败,就返回false,那么,客户端执行远程方法会失败。这种方式可以让客户端连接到服务端,但无法执行服务端的远程方法。
还可以使用getCallingPid()方法获取Pid,去验证客户端的包名,方法与上面介绍的基本一样。
不过,除了上述两种方法,还可以使用其它方式验证权限,比如为Service组件指定android:permission属性等,这里就不进行介绍了。
使用ContentProvider
ContentProvider是Android系统中专门用于不同应用间进行数据数据共享的方式,从这点来看,ContentProvider本身就适合进程间通信。和AIDL一样,它的底层也是通过Binder实现的,不过,系统对其进行了封装,使用过程比AIDL要简单。ContentProvider的具体使用这里就不介绍了,下面主要阐述使用ContentProvider进行IPC需要注意的细节。
创建一个ContentProvider一般需要实现六个抽象方法:onCreate、query、update、insert、delete和getType。根据Binder原理,这六个方法都运行在ContentProvider的进程中,不过,除了onCreate方法由系统回调并运行在主线程中,其它五个方法均由其它进程回调且运行在Binder线程池中。因此,不能在onCreate方法中做耗时操作,并且,其它应用调用另外五个远程方法时,如果这些方法比较耗时,不要在UI线程中调用。
由于query、update、insert、delete这四个方法会涉及到数据源的读写操作,并且它们运行在Binder线程池中,所以在具体实现这些方法时,要实现线程同步操作,保证线程安全。
使用Socket
Socket主要用于网络通信,网络通信的服务端和客户端处于不同的主机,既然这样,处于同一个设备的服务端和客户端就更加能够使用Socket进行通信。因此,可以在Android系统中使用Socket方式进行进程间通信。Socket的具体使用这里就不介绍了,下面主要阐述在Android中使用Socket进行IPC需要注意的地方。
使用Socket进行通信,需要声明网络权限:
1 | <uses-permission android:name="android.permission.INTERNET" /> |
另外,不要在主线程中访问进行网络通信,因为网络操作一般来说都比较耗时,如果放在主线程中会影响应用的响应效率。
Binder连接池
在使用AIDL时,如果一个项目中,有多个业务模块都要使用AIDL提供服务,按照正常的实现方式,会创建多个Service组件去实现这些服务。但是,Service组件是一种系统资源,过多使用会影响系统性能;并且,在一个应用中使用多个Service,用户在查看APP详情的时候,会发现这个应用会有多个服务同时在运行,给用户造成该应用浪费系统资源的印象。为了解决这个问题,需要减少Service的数量,将所有AIDL接口都放在同一个Service中去管理。
要实现用一个Service组件管理多个AIDL接口的需求,就要使用到Binder连接池:
每个业务模块创建自己的AIDL接口,并实现这个接口,不过,不同的业务模块之间不能耦合;实现接口后,每个AIDL模块向服务端提供唯一的标识符和对应的Binder对象。
对于服务端,只需要一个Service组件,提供一个queryBinder接口,这个接口能根据AIDL模块的标识符返回对应的Binder对象给客户端。
对于客户端,通过AIDL模块的标识符调用远程queryBinder方法,获取对应的Binder对象后,就可以使用这个Binder对象调用该模块的远程方法。
Binder连接池的主要作用是将客户端对每个业务模块的Binder请求统一转发到远程的Service中去执行,从而避免了重复创建Service组件,其工作原理如下图所示:

以上阐述了Binder连接池的实现原理,具体实现可以参考《Android开发艺术探索》相关章节。
使用合适的IPC方式
上面介绍了各种IPC方式,但每种IPC方式都有其使用场景,可以根据具体需求选择不同IPC方式。
下表总结了Android中各种IPC方式的优缺点以及使用场景:
| 方式 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| Intent | 简单易用 | 只支持传输Bundle数据 | 四大组件之间的进程间通信 |
| 文件共享 | 简单易用 | 不适合并发场景,无法做到即时通信 | 无并发访问、交换简单数据并且实时性不高的场景 |
| Messenger | 支持一对多串行通信,支持实时通信 | 不太适合高并发场景,不支持RPC,只支持传输Bundle数据 | 低并发的一对多的即时通信,并且无RPC要求 |
| AIDL | 支持一对多并发通信,支持实时通信 | 使用比较复杂,需要处理线程同步 | 一对多通信,或者有RPC需求 |
| ContentProvider | 支持一对多并发数据共享 | 可理解为受约束的AIDL,主要提供对数据源的CRUD操作 | 一对多的进程间的数据共享 |
| Socket | 功能强大,支持一对多并发实时通信 | 实现比较繁琐,不支持直接RPC | 网络数据交换 |