3.3.3 服务组件的进程间通信模型

当界面组件与服务组件绑定后,进程会通过方法的调用进行通信。而在实际应用中,前台界面组件和后台服务组件可能来自不同的应用,这就需要进行进程间通信。Android的进程间通信模型,主要包含三方面的内容。

❑Android的进程间通信模型架构

如图3-9所示,Android的进程间通信模型和传统的远程通信模型非常相似,是典型的代理模式(Proxy Pattern)。在Android中,开发者将需要进行远程通信的接口定义成android.os.IInterface接口的子类型(即图中的MY_API)。该类型会有两个实现者:用在调用者一端的MY_API.Proxy(简称为Proxy对象)和用在功能实现者一端的MY_API.Stub(简称为Stub对象)。

在Proxy对象中,每个函数接口都有一个对应的指令值。当调用者调用这个接口时,Proxy会将数据和指令序列化成一个消息,发送到远端的Stub对象。Stub对象会拆解出对应的指令和数据,并根据指令执行对应的逻辑,将结果返回给接口调用者,整个流程对于调用者而言完全透明。

Proxy数据包的发送和回传工作,是通过android.os.Binder来实现的。在Binder对象中,有一个后台消息循环线程,Proxy传来的消息包会扔到消息队列中等待解析和处理。Stub对象都是Binder的子类型,在服务端被实例化,其接口和实现与Proxy一一匹配,负责将Proxy传递过来的消息进行解包,得到其中的指令和数据。在实际应用中,每个Stub都会有一个实现子类Implement,实际上是由它来真正负责在Binder的后台线程中执行所调用的功能。

3.3.3 服务组件的进程间通信模型 - 图1

图 3-9 Android的进程间通信模型

在界面组件和服务组件绑定的流程中,界面组件可以通过Context.bindService获得IBinder的对象,通过它的静态方法asInterface可以获得Proxy对象实例。界面端使用Proxy对象,就可以实现对服务端功能的远程调用。

对于IPC方法的调用是一个同步的流程,如果执行时间过长,就会阻塞调用方的线程,这时候需要用到异步IPC调用。如图3-10所示,构造一个异步的IPC方法调用,需要传入另一个IInterface对象。它的Stub对象(图中的Callback.Stub)在调用组件,而它的Proxy对象(图中的Callback.Proxy)会随着序列化传输到服务组件,供服务端在操作执行完成后回调通知。服务组件完成操作后,会转换角色扮演调用方,通过Callback.Proxy对象中的方法,将执行结果通知给调用组件。与第一次调用不同的是,这个调用通常是一个非阻塞性质的,即服务组件不等调用组件执行完成便立刻返回,以确保服务组件不会被阻塞,从而继续为其他调用者服务。

❑框架代码自动生成

通过前面介绍不难看出,整个进程通信模型中有固定的类型和方法需要实现,包含很多琐碎而雷同的序列化与反序列化操作。如此机械的工作,当然需要通过自动化的手段来完成。在Android中,是通过AIDL(Android Interface Definition Language)的帮助来自动生成这些框架代码的。

3.3.3 服务组件的进程间通信模型 - 图2

图 3-10 Android的进程间异步方法调用

AIDL是一种接口描述语言,它的语法取自Java,在参数上增加了输入输出方向的控制,一个简单的AIDL文件示例如下:


//声明Java包头,该AIDL文件会生成对应的Java类,并放在gen目录下

package com.duguhome.test.SimpleAidl;

//声明导入的包,通常是另一个AIDL生成的类,用于异步调用函数

import com.duguhome.test.SimpleAidl.ICallback;

//接口声明

interface IMyApi{

//一个简单的参数同步方法

int getId(in String name);

//一个带有输入输出参数的同步方法

void getIds(in String[]names, out Long[]values);

//一个异步方法

void asyncGetId(in String name, in ICallback callback);

};


Android SDK中提供了AIDL的解析工具,它会根据所提供的AIDL文件,自动生成对应的MY_API、MY_API.Proxy、MY_API.Stub等类型的Java文件。开发者只需要继承MY_API.Stub,实现Implement类型,填充真实的执行代码即可,无需关注其他的底层通信细节,这大大提高了效率,降低了开发成本[1]

❑参数序列化

整个进程间通信的流程中,为了将一个进程中的数据传递到另一个进程中,还有一个重要的步骤,就是数据的序列化和反序列化—这是所有进程间通信的基础。

在Android中,负责序列化和反序列化数据的是android.os.Parcel类,它提供了一系列的write和read接口,支持多种类型数据的序列化操作。Parcel支持的数据类型主要有三种。一种是基本数据和它们的列表、数组对象,比如int、long、double、List<String>、byte[]等;一种是实现了android.os.Parcelable接口的子类型对象—子类型通过派生writeToParcel方法和提供构造函数的途径实现序列化相关的操作[2];此外,IBinder和IInterface的子类型对象也可以通过Parcel来序列化,这主要是为了支持服务绑定和异步方法调用。

Parcel序列化后的数据是齐位的二进制流,如此设计是为了效率而不是为了灵活性。这就意味着,写入和读取数据的顺序与类型必须完全一致,稍有偏差,就会触发异常。如果需要传输的数据随着场景的不同有很大的变动,可以选择使用android.os.Bundle对象,它会按照键值对的模式将信息写入二进制数据流,以提高灵活性。

进程间通信机制是Android的重要基础,被广泛地应用在各类核心机制中,因此Parcel类型的实现主要通过C++来完成,上层Parcel对象通过JNI接口进行调用,从而提高了序列化相关操作的执行效率,确保Android系统可以高效运行。

小贴士 在Android开发中,我们时常需要通过Context.getSystemService接口来获得指定的系统服务。这些系统服务,并不是通过服务组件来实现的。它们都位于系统的核心进程中,有独立的线程空间。

通过Context.getSystemService获得的,其实是这些服务的代理对象。这些对象会与真正的服务线程建立连接,通过IPC调用来实现对应的方法。与组件服务相比,系统服务的查询和使用没有利用Intent机制,虽然没那么灵活,但更为直接简便。

[1]Android AIDL的具体构造和使用,可以参见SDK文档:http://developer.android.com/guide/developing/tools/aidl.html。

[2]一个简单的实例参见SDK文档:http://developer.android.com/reference/android/os/Parcelable.html。