前言

本篇博客是我在学习任玉刚老师所著《Android开发艺术探索》一书时的笔记,并且加上一些自己的思考,借此整理归纳,以便理清思维日后回顾。

IPC

什么是IPC?

IPC是Inter-Process Communication的缩写,译为进程间通信或跨进程通信,指两个进程之间进行数据交换的过程。IPC并非Android独有,如Windows的剪贴板、管道、邮槽和Linux的命名管道、共享内存、信号量都是IPC的一种方式。

为什么要用IPC?

  1. 应用因为某些自身需要采用多进程模式实现,如某些模块需要运行在独立的进程中,或者应用想通过多进程来获取多份内存空间。
  2. 当前应用需要向其他应用获取数据,例如ContentProvider就是一种方式。

多进程

Android中使用多进程只有一种办法,那就是给四大组件在AndroidMenifest.xml文件中指定android:process属性,其中进程名以“:”开头的进程属于当前应用的私有应用,其他应用的组件不可以和它跑在同一个进程中。一般来说,使用多进程会造成如下几个方面的问题:

  1. 静态成员和单例模式完全失效(不同进程间并不是同一个对象)
  2. 线程同步机制完全失效(不同进程并不是同一个对象,不能锁住同一个对象)
  3. SharedPreferences的可靠性下降(内部通过读写XML文件实现,不支持并发读写)
  4. Application会多次创建(新的进程分配新的虚拟机,也就是新建了一个应用)

序列化(Serializable和Parcelable)

为什么要序列化?

将对象进行流化,便于在网络或者其他通道上传输。

注意

  1. 序列化和反序列化后的对象,内容完全一样,但并不是同一个对象
  2. 静态成员变量属于类而不属于对象,所以不会参与序列化过程,而用transient关键字标记的成员变量也不参与序列化过程

两者对比

  1. Serializable是java中的序列化接口,使用简单但开销很大,序列化和反序列化过程需要大量的I/O操作。
  2. Parcelable是Android中的序列化接口,使用麻烦但效率很高。
  3. 将对象序列化到存储设备中或者将对象序列化后通过网络传输时,建议使用Serializable;而对于内存序列化,建议使用Parcelable。

Binder

Android中最具特色的IPC是Binder,它是一个实现了IBinder接口的类,使用Binder原因如下:

  1. 从性能的角度
    数据拷贝次数:Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能的角度看,Binder性能仅次于共享内存。

  2. 从稳定性的角度
    Binder是基于C/S架构的,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从稳定性角度看,Binder架构优越于共享内存。

  3. 从安全的角度
    Android为每个应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,Binder可以建立私有通道获取这个UID,在连接服务端之前对这个UID进行权限判断,如果没有访问权限直接返回null拒绝连接;从安全的角度,Binder安全性更高。

  4. 从语言层面的角度
    Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面的角度,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。

使用Binder需要注意的是:

  1. 当客户端发起远程请求时,当前线程会被挂起直至服务端进程返回数据,所以不能在主线程发起远程耗时请求。
  2. 由于服务端的Binder运行在Binder线程池中,所以Binder方法应该采用同步的方法去实现,因为它已经运行在一个线程中了。

实现iPC的方式

一、使用Bundle

因为Bundle本身实现了Parcelable接口,所以它可以很方便在进程间进行数据传输。当然,我们传输的数据必须能够被序列化。
【Android中级工程师】跨进程通信IPC

【Android中级工程师】跨进程通信IPC

【Android中级工程师】跨进程通信IPC

二、使用文件共享

文件共享实现进程间通信即在A进程把数据写入文件,而在B进程把数据从文件中读取出来。由于Android基于Linux的缘故,使得其可以并发的执行读/写文件,当然这会导致一些问题,例如我们读出来的内容可能不是最新的,所以如果要这么做的话最好考虑使用线程同步来限制多个线程的写操作。所以共享文件的方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写问题。
SharedPreferences也是一种文件共享的方式,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,面对高并发时,容易导致数据丢失。因此不建议在进程间通信中使用SharedPreferences。

三、使用Messenger

Messenger译为信使,而Message译为消息,显然,我们只需要把需要传递的数据放进Message通过Messenger在不同进程中传递即可以完成IPC。Messenger是一种轻量级的IPC方案,它的底层实现仍然是AIDL,它只是对AIDL做了封装。同时,由于它一次只处理一个请求,所以不需要考虑线程同步问题,因为根本不会出现并发执行的情形。同时这也说明了一个问题,如果大量消息同时发送到服务端,而服务端只能一个一个处理,这样就会导致后面的消息处理要等待很长时间,这就是Messenger的局限性。其次,Messenger的主要作用是用来传递消息,如果我们需要跨进程调用服务端的方法,那么Messenger也无能为力。它的工作原理如下:
【Android中级工程师】跨进程通信IPC

四、使用AIDL

利用AIDL完成跨进程通信的原理大致是:在服务端创建一个service监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的借口在这个文件中声明,最后在service中实现这个AIDL接口。在客户端则先绑定服务端的service,将服务端返回的Binder对象转成AIDL接口所属的类型,就可以调用AIDL中的方法了。需要注意的是,AIDL接口中只支持方法,不支持声明静态常量。

死亡监听

在某些情况下,例如服务端进程意外停止了,Binder可能意外死亡,这时我们需要重新连接服务。这里有两种方法:

  1. 为Binder设置DeathRecipient设置监听,当Binder死亡时,我们会收到binderDied方法的毁掉,在其中我们可以重连远程服务。
  2. 在onServiceDisconnected中重连远程服务。

两者的区别是,第二种方法在客户端的UI线程中被回调,而第一种方法在客户端的Binder线程池中回调而不能访问UI。

权限验证

权限验证主要是防止我们的远程服务任何人都可以连接,所以我们要对申请我们服务的客户端请求进行验证,主要有两种方式:

  1. 在onBind中验证,验证不通过直接返回null,这样客户端就无法绑定服务。验证方式可以通过permisson,在AndroidManifest.xml文件中声明权限。
  2. 在服务端的onTransact方法中验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。这里我们也可以和第一种一样用permisson来验证,当然也可以采用UID和PID来验证。

五、使用ContentProvider

ContentProvider底层实现仍然是Binder,但是由于系统已经为我们做了封装,所以使用起来比较简单。

六、使用Socket

Socket是java中常用的通信工具,它分为流式套接字TCP和用户数据报套接字UDP。两个进程通过Socket来实现信息的传输,由于Socket本身可以支持传输任意字节流,所以这种IPC方式也支持传输任意字节流。

Binder连接池

前面提到使用AIDL的大致流程是:首先创建一个Service和一个AIDL接口,接着创建一个类继承AIDL接口中的Stub类并实现Stub中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端就可以绑定服务端Service,建立连接后就可以访问远程服务端的方法了。
而Binder连接池的作用是:当我们项目的模块越来越多时,每个模块都要相应的AIDL来进行进程间通信,那么每个模块都需要一个单独的Service,这就导致了我们项目变得十分庞大。那我们可以把所有的Service集中成一个Service,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的binder对象就可以进行远程方法的调用了。

各种IPC方式比较

名称 优点 缺点 使用场景
Bundle 简单易用 只能传输Bundle支持的数据类型 四大组件间的进程间通信
文件共享 简单易用 无法做到即时、高并发通信 无并发访问情形、交换简单实时性不高的场景
AIDL 功能强大,支持一对多并发通信,支持实时通信 使用稍复杂,需要处理好线程同步 一对多通信且有RPC需求
Messenger 功能一般, 支持一对多并发通信,支持实时通信 高并发较难,不支持RPC,只能传输Bundle支持的数据类型 低并发的一对多即时通信,无RPC需求,或无需返回结果的RPC需求
ContentProvider 在数据源访问方面功能强大,支持一对多并发数据共享,可通过Call方法扩展其他操作 可理解为受约束的AIDL 一对多的进程间的数据共享
Socket 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 实现繁琐,不支持直接的RPC 网络数据交换

相关文章: