Android中的IPC方式

上一篇文章介绍了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的主要步骤:

  1. 在服务端实现一个Handler,用来处理客户端发送的消息;
  2. 在服务端使用步骤1中的Handler初始化Messenger对象;
  3. 在服务端的onBind()方法中,使用步骤2中的Messenger对象创建一个Binder对象,并返回给客户端;
  4. 在客户端中使用步骤3中的Binder对象初始化一个Messenger对象,它用来给服务端发送Message对象;
  5. 服务端接在Handler中接收到Message对象后,进行相应的处理;

在服务端创建Messenger接口的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MessengerService extends Service {
/** Command to the service to display a message */
static final int MSG_SAY_HELLO = 1;

/**
* Handler of incoming messages from clients.
*/
class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SAY_HELLO:
Toast.makeText(getApplicationContext(), "hello!", Toast.LENGTH_SHORT).show();
break;
default:
super.handleMessage(msg);
}
}
}

/**
* Target we publish for clients to send messages to IncomingHandler.
*/
final Messenger mMessenger = new Messenger(new IncomingHandler());

/**
* When binding to the service, we return an interface to our messenger
* for sending messages to the service.
*/
@Override
public IBinder onBind(Intent intent) {
Toast.makeText(getApplicationContext(), "binding", Toast.LENGTH_SHORT).show();
return mMessenger.getBinder();
}
}

在客户端使用Binder对象创建Messenger对象,并发送消息的示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class ActivityMessenger extends Activity {
/** Messenger for communicating with the service. */
Messenger mService = null;

/** Flag indicating whether we have called bind on the service. */
boolean mBound;

/**
* Class for interacting with the main interface of the service.
*/
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mService = new Messenger(service);
mBound = true;
}

public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mService = null;
mBound = false;
}
};

public void sayHello(View v) {
if (!mBound) return;
// Create and send a message to the service, using a supported 'what' value
Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}

@Override
protected void onStart() {
super.onStart();
// Bind to the service
bindService(new Intent(this, MessengerService.class), mConnection,
Context.BIND_AUTO_CREATE);
}

@Override
protected void onStop() {
super.onStop();
// Unbind from the service
if (mBound) {
unbindService(mConnection);
mBound = false;
}
}
}

上面的示例没有展示服务端如何回复客户端。不过,如果服务端需要回复客户端,有一点非常关键,那就是当客户端发送消息的时候,需要把接收服务器端回复的Messenger通过Message的replyTo参数传递给服务端,作为通信的桥梁。

通过Messenger进行IPC的原理图如下所示:

Messenger通信原理

使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements

/** Example service interface */
interface IRemoteService {
/** Request the process ID of this service, to do evil things with it. */
int getPid();

/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}

实现接口

创建完AIDL接口后,Build项目,这时会根据aidl文件生成一个对应Java接口文件。这个接口包含了一个名为Stub的子类,它是一个Binder接口,服务端需要实现这个接口提供对应的服务。

1
2
3
4
5
6
7
8
9
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
return Process.myPid();
}
public void basicTypes(int anInt, long aLong, boolean aBoolean,
float aFloat, double aDouble, String aString) {
// Does nothing
}
};

在实现AIDL接口定义的方法时,需要注意:

  • 要考虑多线程问题;
  • 默认情况下,客户端调用服务端方法(RPC)是同步的,如果客户端的UI线程进行RPC,并且这个RPC比较耗时,会造成客户端ANR。所以,在需要的时候,在工作线程中进行RPC;
  • 进行RPC时抛出的Exception不会被发送到客户端;

将接口暴露给客户端

服务端实现AIDL接口后,需要将接口暴露给客户端。一般是通过Service组件,在其onBind()方法中将实现的Binder接口返回给客户端,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RemoteService extends Service {
@Override
public void onCreate() {
super.onCreate();
}

@Override
public IBinder onBind(Intent intent) {
// Return the interface
return mBinder;
}

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
return Process.myPid();
}
public void basicTypes(int anInt, long aLong, boolean aBoolean,
float aFloat, double aDouble, String aString) {
// Does nothing
}
};
}

如果客户端和服务端属于不同的项目,需要将服务端的aidl文件复制到客户端,这时客户端才能声明AIDL定义的接口,并进行使用。

当客户端调用bindService()方法连接服务端时,onServiceConnected()回调方法会接收到服务端返回的Binder接口,将其转换为AIDL定义接口后,就可以调用服务端对应的方法了,示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IRemoteService mIRemoteService;

private ServiceConnection mConnection = new ServiceConnection() {
// Called when the connection with the service is established
public void onServiceConnected(ComponentName className, IBinder service) {
// Following the example above for an AIDL interface,
// this gets an instance of the IRemoteInterface, which we can use to call on the service
mIRemoteService = IRemoteService.Stub.asInterface(service);
}

// Called when the connection with the service disconnects unexpectedly
public void onServiceDisconnected(ComponentName className) {
Log.e(TAG, "Service has unexpectedly disconnected");
mIRemoteService = null;
}
};

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
2
IBinder key = listener.asBinder();
Callback value = new Callback(listener, cookie);

可以看到,Map结构的key值是Listener对应的Binder对象。之所以会这样,是因为Listener对象虽然是跨进程的,在客户端和服务端会是不同的对象,不过,这个跨进程的Listener在底层对应的Binder对象是同一个。正是利用这个特性,RemoteCallbackList可以正确删除掉对应的Listener对象。

同时,RemoteCallbackList内部自动实现了线程同步的功能,并且,在客户端进程意外终止后,也能够自动删除客户端注册的Listener对象。

RemoteCallbackList常用的API有:

  • register和unregister:用来注册和注销客户端对应的Listener对象;
  • beginBroadcast和finishBroadcast:开始使用和结束使用RemoteCallbackList对象,必须成对使用;
  • getBroadcastItem(int pos):根据pos获取对应的Listener对象;

多线程问题

  1. 客户端调用服务端的远程方法时,远程方法运行在服务端的Binder线程池中,同时,客户端线程会被阻塞,如果这个线程是UI线程,并且远程方法执行比较耗时,会造成客户端出现ANR,所以,不要在UI线程中调用耗时的远程方法。

  2. 服务端的远程方法(通过实现Binder接口的方法)本身就运行在服务端的Binder线程池中,所以,服务端的远程方法本身就可以执行大量耗时操作,切记不要在远程方法中开启线程执行异步任务,除非明确知道开启异步线程的目的。

  3. 当服务端调用客户端Listener中的方法时,该方法也运行在Binder线程池中,不过是客户端的线程池,所以,相对于服务端,Listener中的方法也是远程方法,因此,也不能在服务端(要注意Service组件运行在UI线程)调用客户端的耗时远程方法。所以,要确保客户端的耗时远程方法运行在服务端的非UI线程,否则,会导致服务端无法及时响应。

  4. 客户端的远程方法运行在客户端的Binder线程池中,因此,不能在其远程方法中修改UI。如果要修改UI,需要使用Handler切换到UI线程。

  5. 服务端的Binder连接有可能意外断开,为了程序的健壮性,客户端可以在断开时,重新连接服务端。有两种方法实现这个功能:第一种是客户端连接服务端成功后,使用Binder.linkToDeath()方法设置DeathRecipient监听器,这样,在Binder断开时,客户端会收到binderDied()回调方法;第二种是在客户端连接服务端时,重载onServiceDisconnected()方法。这两种方法的区别是,binderDied()回调方法运行在客户端的Binder线程池中,而onServiceDisconnected()方法运行在UI线程中。

权限控制

默认情况下,远程服务任何客户端都能连接,这是不安全的,可以给服务加入权限验证功能。通常有两种方法:

  • 在onBind()方法中验证

可以在服务端的onBind()方法中进行权限验证。下面介绍在onBind()方法中使用Permission验证:

1.在Manifest文件中声明所需的权限

1
2
3
<permission
android:name="com.example.ipc.permission.ACCESS_DADA"
android:protectionLevel="normal" />

2.在onBind()方法中做权限验证

1
2
3
4
5
public onBind(Intent intent) {
int check = checkCallingSelfPermission("com.example.ipc.permission.ACCESS_DADA");
if (check == PackageManager.PERMISSION_DENIED) return null;
return mBinder;
}

如果权限不通过,就直接返回null,那么客户端就无法访问这个服务端。

3.如果客户端需要绑定声明权限的服务端,需要在Manifest中声明权限

1
<uses-permission android:name="com.example.ipc.permission.ACCESS_DADA" />

这种方式也可以用于Messenger中。

  • 在onTransact()方法中验证

可以在服务端实现Binder接口时,重载onTransact()方法进行权限验证。下面介绍在onTransact()方法中获取客户端的Uid验证包名:

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) 
throws RemoteException {
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
if (packageName == null || !packageName.startsWith("com.example.ipc")) {
return fasle;
}

return super.onTransact(code, data, reply, 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
2
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

另外,不要在主线程中访问进行网络通信,因为网络操作一般来说都比较耗时,如果放在主线程中会影响应用的响应效率。

Binder连接池

在使用AIDL时,如果一个项目中,有多个业务模块都要使用AIDL提供服务,按照正常的实现方式,会创建多个Service组件去实现这些服务。但是,Service组件是一种系统资源,过多使用会影响系统性能;并且,在一个应用中使用多个Service,用户在查看APP详情的时候,会发现这个应用会有多个服务同时在运行,给用户造成该应用浪费系统资源的印象。为了解决这个问题,需要减少Service的数量,将所有AIDL接口都放在同一个Service中去管理。

要实现用一个Service组件管理多个AIDL接口的需求,就要使用到Binder连接池:

  1. 每个业务模块创建自己的AIDL接口,并实现这个接口,不过,不同的业务模块之间不能耦合;实现接口后,每个AIDL模块向服务端提供唯一的标识符和对应的Binder对象。

  2. 对于服务端,只需要一个Service组件,提供一个queryBinder接口,这个接口能根据AIDL模块的标识符返回对应的Binder对象给客户端。

  3. 对于客户端,通过AIDL模块的标识符调用远程queryBinder方法,获取对应的Binder对象后,就可以使用这个Binder对象调用该模块的远程方法。

Binder连接池的主要作用是将客户端对每个业务模块的Binder请求统一转发到远程的Service中去执行,从而避免了重复创建Service组件,其工作原理如下图所示:

Binder连接池

以上阐述了Binder连接池的实现原理,具体实现可以参考《Android开发艺术探索》相关章节。

使用合适的IPC方式

上面介绍了各种IPC方式,但每种IPC方式都有其使用场景,可以根据具体需求选择不同IPC方式。

下表总结了Android中各种IPC方式的优缺点以及使用场景:

方式 优点 缺点 使用场景
Intent 简单易用 只支持传输Bundle数据 四大组件之间的进程间通信
文件共享 简单易用 不适合并发场景,无法做到即时通信 无并发访问、交换简单数据并且实时性不高的场景
Messenger 支持一对多串行通信,支持实时通信 不太适合高并发场景,不支持RPC,只支持传输Bundle数据 低并发的一对多的即时通信,并且无RPC要求
AIDL 支持一对多并发通信,支持实时通信 使用比较复杂,需要处理线程同步 一对多通信,或者有RPC需求
ContentProvider 支持一对多并发数据共享 可理解为受约束的AIDL,主要提供对数据源的CRUD操作 一对多的进程间的数据共享
Socket 功能强大,支持一对多并发实时通信 实现比较繁琐,不支持直接RPC 网络数据交换
坚持原创技术分享,您的支持将鼓励我继续创作!