Android 开发艺术探索学习笔记(二)

结合 官方文档 阅读《Android 开发艺术探索》时所做的学习笔记。本篇记录第二章:IPC 机制。

What? How? Why?

什么是 IPC?

所谓的 IPC 是指进程间通信(InterProcess Communication 的缩写),我们知道 Linux 里可以通过管道、signal 等进行重定向、跨进程通信等,Android 中同样如此。一般情况下只要应用中使用了多个进程,那么就会涉及到进程间通信的问题。另外,由于每一个 Android 应用都是一个沙箱,独享一个进程,所以应用间数据共享同样也需要通过 IPC。

有哪些 IPC 的方式?

Android 中进程间通信的方式有很多,比如最常见的通过 Intent.putExtra 以及 Content Provider,通过文件共享(需要注意并发读/写问题),通过 Socket,以及最重要的一种 IPC 方式,通过 绑定服务,其中最重要的部分是 Binder,MessagerAIDL 底层实现也是通过 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 接口类型然后调用获取结果。

具体步骤如下:

  1. 创建 .aidl 接口

与创建普通 Java 接口类似,只不过只支持下面一些数据类型:

  • 基本数据类型
  • String 和 CharSequence
  • 实现了 Parcelable 接口的对象
  • List(具体实际使用的是 ArrayList)和 Map(具体实际使用的是 HashMap),其中的元素必须全都支持 AIDL
  • AIDL 接口,使用时必须显式导入

另外,使用 AIDL 传递 Parcelable 对象时,还需要我们为该 Parcelable 对象创建一个 .aidl 文件,如下:

1
2
3
package your.package.name

parcelable YourParcelableClass;

再之,AIDL 接口方法的参数必须标明方向 in out inoutin 表示输入型参数,out 表示输出型参数,inout 两者皆可,基本数据类型默认为 in。不同参数类型的 Binder 在再编码marshall,不知道如何翻译,将数据序列化、传输、接收、解序列化的过程)时所需的步骤不同,标明类型后,Binder 可以跳过某些步骤从而达到节省开销的目的。具体解释见:“In/out/inout” in a AIDL interface parameter value?

创建好 AIDL 接口后,gradle sync 后 IDE 会为我们生成对应的 Java 文件。

  1. 实现 AIDL 接口(其实是接口中的 Stub 类,即 Binder 真正的实现类)

在服务端 Service 中,我们需要实现接口中的 Stub 类,然后在 onBind() 方法中返回供客户端调用。需要注意的是:

  • 来自客户端的调用,不一定是执行在主线程的,而且有可能多个客户端与服务端同时建立连接,所以需要注意线程安全的问题。
  • 默认情况下,远程调用是同步执行的,所以如果客户端在主线程发生 RPC,需要考虑调用是否耗时,否则会造成 ANR,最好在子线程调用。
  • 客户端不会接收到服务端抛出的异常。
  1. 客户端的实现

客户端通过 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 网络数据交换

系列文章