结合 官方文档 阅读《Android 开发艺术探索》时所做的学习笔记。本篇记录第二章:IPC 机制。
What? How? Why?
什么是 IPC?
所谓的 IPC 是指进程间通信(Interprocess communication 的缩写),我们知道 Linux 里可以通过管道、signal 等进行重定向、跨进程通信等,Android 中同样如此。一般情况下只要应用中使用了多个进程,那么就会涉及到进程间通信的问题。另外,由于每一个 Android 应用都是一个沙箱,独享一个进程,所以应用间数据共享同样也需要通过 IPC。
有哪些 IPC 的方式?
Android 中进程间通信的方式有很多,比如最常见的通过 Intent.putExtra 以及 Content Provider,通过文件共享(需要注意并发读/写问题),通过 Socket,以及最重要的一种 IPC 方式,通过 绑定服务,其中最重要的部分是 Binder,Messager 和 AIDL 底层实现也是通过 Binder。
为什么需要 IPC?
首先,当应用 A 启动的时候,Android 系统会为它单独指定一个进程,分配一个 User ID,于是它只能在这个进程里执行自己的代码,访问自己的数据空间。但是如果我们为应用 A 指定了多个进程,并且想要调用另一个进程中的代码,那么我们是无法直接访问另一个进程中的内存空间的,所以就需要某种方式或者说通过某个桥梁,建立起进程间的连接。这个方式可以称为 remote procedure calls (RPCs),而借用的桥梁就是 Binder。
简单来说就是将方法调用以及数据,分解成操作系统能够读取的程度,然后把它从当前进程和地址空间中传送到目标进程和地址空间,重新组合起来之后再进行调用,最后将结果用同样的方式返回给请求发生的进程。
More details
IPC 基础概念
序列化
Serializable 接口
- 只要实现 Serializable 接口并提供
serialVersionUID
,系统会为我们自动完成序列化的工作。 - 系统通过
servialVersionUID
来标记需要序列化的类,如果我们不提供,系统会为我们默认生成一个,而使用默认生成的servialVersionUID
的时候,如果序列化类发生改变则servialVersionUID
也会发生改变,此时反序列化就会失败报错。所以必须手动指定servialVersionUID
值。 - 可以使用
transient
关键字标记不需要序列化的成员变量。
Parcelable 接口
- 通过
writeToParcel
完成序列化(一系列的Parcel.writeXXX
)。 - 通过 CREATOR 来标记如何创建序列化对象以及数组,并通过构造方法中的一系列
Parcel.readXXX
完成反序列化。 - 反序列化时,如果存在可序列化的属性,即使用
Pacel.readParcelable
时需要传递当前线程的类加载器,否则会报 ClassNotFound 的错误。 - 一般情况下,
discribeContents
都应该返回 0,只有在存在文件描述符时才需要返回 1。
Binder
Binder 实现了 IBinder 接口,在跨进程通信中,它是客户端与服务端通信的媒介,当绑定服务时,服务端会返回一个 Binder 对象,而客户端正是通过这个 Binder 对象来获取服务端的数据以及进行远程方法调用。
一般的 Service 如果不涉及进程间通信,那么只要实现 IBinder 接口就可以了,所以我们以最基本的 AIDL 作为例子来解释 Binder 的工作原理。AIDL 实际上是 Android 为我们封装好的接口定义语言,它自动为我们生成 Binder 的实现代码。
具体而言,一个 Binder 的实现通常需要有以下部分:
-
DESCRIPTOR
,该 Binder 的唯一标识。 - 供客户端调用的方法,具体实现交给内部类 Stub。
- Stub 中的
asBinder()
方法,返回当前 IBinder 对象。 - Stub 中的
asInterface(IBinder obj)
方法,将 IBinder 转换为客户端实际需要的接口类型(即当前接口)的对象,如果是在同一进程中,则直接返回当前对象,否则就需要创建一个 Proxy 对象并返回,因为需要通过 transact() 方法进行跨进程传递数据、完成方法调用并返回结果。 - Stub 中的
onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
方法,实际对客户端跨进程请求进行处理的地方,通过 code 确定请求方法,通过 data 获取参数,向 reply 中写入返回值,如果返回结果为 true 表示请求成功,false 表示无法识别请求 code,我们也可以通过此方法来做权限验证。另外,此方法运行在服务端的 Binder 线程池中。 - Proxy 类才是真正 IPC Binder 的实现类,由客户端发起调用。如果是在同一进程中,会直接调用 Stub 中的方法,而如果是跨进程,则需要使用到 Proxy类。具体过程如下:
- 将输入对象、输出对象、返回值写入 Parcel data;
- 然后发起远程调用,即执行 transact() 方法,当前线程会被挂起;
- 然后再调用服务端的 onTransact() 方法,获取返回结果(如果有的话)后,再从返回值 reply 中读取结果并返回,客户端当前线程得以继续执行。
以上最关键的部分是最后的 Proxy 类以及 onTransact() 方法。
关于 AIDL 的具体使用方式请看下面的 AIDL 部分。
IPC 方式介绍
Bundle
Intent.putExtra(Bundle),数据必须是可以序列化的,比如原始数据类型,实现 Parcelable 接口或者 Serializable 接口的对象,基本数据类型或者实现了 Parcelable 的 ArrayList/SparceArray 等。
文件共享
对数据格式没有要求,但是需要注意并发读写的问题。
SharedPreferences 是个特例,它会在内存中保存一份拷贝,使用 apply() 时进行异步提交(先保存到内存然后异步提交保存到文件),而使用 commit() 时会阻塞并返回结果,它是单进程模式下的单例,所以多进程明显就变得非常不可靠。
Messager
轻量级的 IPC,底层使用的是 AIDL。
- 服务端创建一个 Service 处理连接请求,创建一个 Handler 对象,对客户端传递过来的信息进行接收处理(通过
what
获取类型,通过arg1
/arg2
/Bundle
获取数据,通过replyTo
获取客户端传递过来的 Messager),再用它创建 Messager 对象(new Messager(Handler)),在 onBind 中返回 Messager 的 IBinder。 - 客户端同样需要创建 Messager,只要通过 ServiceConnection 与服务端建立连接,然后获取到 Server 端的 IBinder,然后从消息池(Message.obtain())中获取 Message,并传递数据 Messager.send(Message),传递数据同样可以通过 [arg1, arg2, what, Bundle, replyTo 以及 object],其中使用 replyTo 传递需要服务端返回的 Messager。
- Message 支持的数据类型中,object 比较特殊,它在单进程中比较实用,但是跨进程限制就比较大了,只能传递系统中实现了 Parcelable 接口的对象,比如 Rect、Point 等。
AIDL
Messager 适合跨进程传递消息,如果有大量请求或者需要跨进程方法调用,那么可能就需要使用到 AIDL 了。
- 服务端:我们需要创建一个 Service 监听客户端的连接请求,然后在 .aidl 文件中声明需要暴露给客户端的接口,再在 Service 中实现这个接口。
- 客户端:绑定 Service,然后将 IBinder 转换为定义好的 AIDL 接口类型然后调用获取结果。
具体步骤如下:
- 创建
.aidl
接口
与创建普通 Java 接口类似,只不过只支持下面一些数据类型:
- 基本数据类型
- String 和 CharSequence
- 实现了 Parcelable 接口的对象
- List(具体实际使用的是 ArrayList)和 Map(具体实际使用的是 HashMap),其中的元素必须全都支持 AIDL
- AIDL 接口,使用时必须显式导入
另外,使用 AIDL 传递 Parcelable 对象时,还需要我们为该 Parcelable 对象创建一个 .aidl
文件,如下:
package your.package.name
parcelable YourParcelableClass;
再之,AIDL 接口方法的参数必须标明方向 in
out
inout
,in
表示输入型参数,out
表示输出型参数,inout
两者皆可,基本数据类型默认为 in
。不同参数类型的 Binder 在再编码(marshall,不知道如何翻译,将数据序列化、传输、接收、解序列化的过程)时所需的步骤不同,标明类型后,Binder 可以跳过某些步骤从而达到节省开销的目的。具体解释见:“In/out/inout” in a AIDL interface parameter value?
创建好 AIDL 接口后,gradle sync 后 IDE 会为我们生成对应的 Java 文件。
- 实现 AIDL 接口(其实是接口中的 Stub 类,即 Binder 真正的实现类)
在服务端 Service 中,我们需要实现接口中的 Stub 类,然后在 onBind() 方法中返回供客户端调用。需要注意的是:
- 来自客户端的调用,不一定是执行在主线程的,而且有可能多个客户端与服务端同时建立连接,所以需要注意线程安全的问题。
- 默认情况下,远程调用是同步执行的,所以如果客户端在主线程发生 RPC,需要考虑调用是否耗时,否则会造成 ANR,最好在子线程调用。
- 客户端不会接收到服务端抛出的异常。
- 客户端的实现
客户端通过 bindService() 与服务端建立连接后,通过 onServiceConnected() 回调中获取 binder
实例,然后再通过 YourAIDLInterface.Stub.asInterface()
将 binder
实例类型转换为所需要的 AIDL 接口(如果客户端位于不同的应用中,则必须也要保留一份 .aidl
文件,以便生成 AIDL 接口)。
需要注意的是,如果我们使用了回调的话,会遇到无法解注册的问题,本质是因为回调接口在从客户端传递到服务端的时候,已经不是同一个对象了,想要成功解注册我们需要使用 RemoteCallbackList
,它是线程安全的(原理:通过遍历所有 Binder 对象--我们的 listener 也是 Binder 对象--然后找到对应的 listener 再删除)。
另外还要注意 Binder 意外死亡(DeathRecipient
或 onServiceDisconnected(),前者在 Binder 线程池中被调用,后者在 UI 线程中调用)以及权限验证(onBind() 或 onTrasact() 中做处理)的问题。具体请查看例子:Chapter_2/aidl
ContentProvider
参考文档
Socket
分为 TCP 套接字(Java 中对应的实现使用 ServerSocket
)和 UDP 套接字(Java 中对应的实现使用 DatagramSocket
),具体例子见:Chapter_2/socket
Binder 连接池
Binder 连接池的主要作用是将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行,从而避免重复创建 Service,节省系统资源。
工作机制:每个模块创建并实现自己的 AIDL 接口,然后向服务端提供自己的唯一标识和对应的 Binder 对象,服务端只是需要一个 Service,然后提供一个 queryBinder 接口,根据业务模块的标识来返回相应的 Binder 对象,然后再由客户端发起远程方法调用。
具体例子见:Chapter_2/binderpool
优缺点对比
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输 Bundle 支持的数据类型 | 四大组件间的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无无法做到进程间的即时通信 | 无并发访问情形,交换简单的数据,实时性不高的场景 |
Messenger | 功能一般,支持一对多串行通信,支持实时通信 | 不能很好处理高并发情形,不支持 RPC,数据通过 Message 进行传输,因此只能传输 Bundle 支持的数据类型 | 低并发的一对多即时通信,无 RPC 需求,或者不需要返回结果的 RPC 需求 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有 RPC 需求 |
Content Provider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作 | 可以理解为受约束的 AIDL,主要提供数据源的 CRUD 操作 | 一对多的进程间的数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点烦琐,不支持直接的 RPC | 网络数据交换 |