进程间通信方式
在Android
开发中我们可以通过Intent
、ContentProviders
来实现进程间通信,如果不限于Android
特有的话,我们还可以使用File
、Socket
等方式,反正只要进程间能交换信息就行了。
像Intent
,我们平时使用的时候好像都没感觉出是在进程间通信。其实Android
中进程间的通信是非常频繁的,应用里打开一个新的Activity
都涉及到了进程间通信,应用里调用打电话、调用浏览器等等都涉及到了。
实际上Intent
、ContentProviders
都是对Binder
更高级别的抽象,方便我们平时使用。
常用方式
上面说到的一些方式都是系统经过高度封装的,而我们的业务需求可能比较特别,使用上面的方式可能不是特别适合,比如:“我们的音乐播放器希望在独立的进程中播放音乐”。
我们至少得控制音乐的开始、暂停、显示进程这些功能吧,那就需要进程间的通信了。这个时候使用系统经过高度封装的方式都好像显得不太灵活。根据官方文档我们发现有两种相对底层一些的方式,Messenger
和AIDL
。
在相对底层一点的进程间通信,Messenger
是最简单的方式,Messenger
会在单一线程中创建包含所有请求的队列,这样我们就不需要处理线程安全方面的事宜。
Messenger
实际上是以AIDL
作为其底层结构的。
Messenger的用法
单向通信
客户端进程相关代码:
public class MainActivity extends AppCompatActivity {
private Messenger mService = null;
// 绑定远程服务成功后相应回调方法
private ServiceConnection mServiceConnection = new ServiceConnection() {
// 绑定成功后会调用该方法
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new Messenger(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this, RemoteService.class);
// 绑定服务
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
}
/**
* 点击按钮向远程服务发送消息
* @param view
*/
public void onClick(View view) {
// 获取一个what值为0的消息对象
Message msg = Message.obtain(null, 0);
try {
// 将消息对象通过Messenger传递到远程服务器
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
protected void onStop() {
super.onStop();
unbindService(mServiceConnection);
}
}
布局XML代码就不贴了,很简单,就一个按钮。
上面的代码也很简单,就得我们平常绑定服务的做法是一样的,唯一的区别就是在绑定成功回调方法onServiceConnected()
中我们根据返回的IBinder
实例化了一个Messenger
对象,当我们点击按钮的时候,通过该Messenger
对象发送一个消息到远程服务端。
服务端进程代码:
/**
* 远程服务端
*/
public class RemoteService extends Service {
// 用来处理客户端传过来的消息
class ServerSideHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
Log.e("Fiend", "我是远程服务端,我收到客户端传递过来的信息了。");
break;
}
}
}
// 实例化一个Messenger对象,并传入Handler
final Messenger mMessenger = new Messenger(new ServerSideHandler());
/**
* 客户端绑定服务端的时候将调用该方法
* @param intent
* @return
*/
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
服务端的代码也很简单,创建一个Handler
来处理消息,实例化一个Messenger
与该Handler
关联,最后通过onBind()
方法将Messenger
的IBinder
给返回。在客户端通过该IBinder
重建一个Messenger
。
我们来看一下运行结果:
确实成功了,而且也确实是在两个进程间。想要让服务运行在别的进程只需要声明的时候指定它的android:process
属性就可以了。
但是我们只是客户端向服务端发送了信息,那服务端如何向客户端发送信息呢?
双向通信
客户端改动地方:
/**
* 点击按钮向远程服务发送消息
* @param view
*/
public void onClick(View view) {
// 获取一个what值为0的消息对象
Message msg = Message.obtain(null, 0);
// 将客户端的Messenger对象放到消息中传递到服务端
msg.replyTo = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
Log.e("Fiend", "我是客户端,收到服务端的回复了");
break;
}
}
});
try {
// 将消息对象通过Messenger传递到远程服务器
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
客户端代码只需要在发送消息之前将本地的一个Messenger
对象放到消息里一起传递到远程服务端即可。
服务端改动地方:
// 用来处理客户端传过来的消息
class ServerSideHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
Log.e("Fiend", "我是远程服务端,我收到客户端传递过来的信息了。");
try {
// 通过客户端的Messenger回复一个what值为1的消息
msg.replyTo.send(Message.obtain(null, 1));
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
}
}
服务端改动的代码也非常简单,只是在收到客户端消息的时候, 通过客户端的Messenger
回复一个消息。这样就实现了本地客户端与远程服务端的通信了。
对于大多数的应用来说,
Messenger
就能满足IPC的需求了,完全没必要使用AIDL
,而且Messenger
比AIDL
简单得多。如果对于服务需要执行多线程处理的,则应使用AIDL
,否则使用Messenger
就可以了。
AIDL的用法
使用AIDL
和使用Messenger
的步骤基本上是类似的。使用AIDL
需要自己定义好一个接口作为客户端和服务端通信的规则,手工写一个这样的接口比较复杂,所以Android
给我们提供了一个工具来自动生成。
想要自动生成通信的接口,则需要创建一个以.aidl
结尾的文件,然后按平常我们定义接口的方式做就好了。下面以Android Studio
来讲解生成过程。
- 新建一个项目,名字随便取:AIDLExample
- 将工程目录结构以
Android
的形式展示:
- 点击项目,右键,新建一个
AIDL
文件:
- 打开新建的AIDL文件
IMyAidlInterface.aidl
,编写通信规则:
- 编写完
IMyAidlInterface.aidl
后,需要重新Build
一下项目,然将工程目录结构以Project
的形式展示,就可以找到生成的真正接口:
至此AIDL
接口就定义好了,剩下的步骤比较简单,和之前讲过的类似。
我们先来编写服务端,直接新建一个Service
并在配置文件中将其配置为android:process=":remote"
,确保它运行在另一个进程中。
// 服务端
public class MyService extends Service {
IMyAidlInterface.Stub iBinder = new IMyAidlInterface.Stub() {
// 我们在aidl文件中定义的通信规则
@Override
public String getMsg() throws RemoteException {
return "我来自MyService";
}
};
@Override
public IBinder onBind(Intent intent) {
return iBinder;
}
}
代码很简单,在绑定的时候将带有我们自己定义的规则的IBinder
返回给客户端。XXX.Stub iBinder = new XXX.Stub() {···}
这样的写法是固定的,记住就好了,将XXX
替换成你的AIDL
接口名称就可以了。
我们来看一下客户端代码:
// 客户端
public class MainActivity extends AppCompatActivity {
private IMyAidlInterface mService;
private boolean isBound;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 绑定服务端
Intent intent = new Intent(this, MyService.class);
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
}
// 绑定回调
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 获取AIDL接口对象,这样就可以用来通信了
mService = IMyAidlInterface.Stub.asInterface(service);
isBound = true;
}
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
isBound = false;
}
};
// 按钮点击回调方法
public void btnClick(View view) {
if (isBound) {
try {
// 调用服务端方法
String result = mService.getMsg();
Log.e("Fiend", "客户端:" + result);
} catch (RemoteException e) {
e.printStackTrace();
}
} else {
Log.e("Fiend", "还没有绑定成功");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isBound) {
unbindService(mServiceConnection);
}
}
}
看起来代码有点多,其实并没有什么陌生的内容,都是我们平时非常熟悉的一些代码。应用启动后就绑定远程服务端,点击按钮调用远程服务端的方法,获取到后将结果打印出来。结果如下:
成功调用另一个进程中的方法。
onServiceConnected()
方法里的这句代码mService = IMyAidlInterface.Stub.asInterface(service);
,属于固定写法,和之前的服务端写法一样,记住就好了。
上面这种IPC方式是属于同步的,所谓同步是指,客户端调用后会等待服务端返回后才会继续向下执行。我们来修改一下客户端代码:
public void btnClick(View view) {
if (isBound) {
try {
Log.e("Fiend", "开始调用服务端方法");
// 调用服务端方法
String result = mService.getMsg();
Log.e("Fiend", "客户端:" + result);
} catch (RemoteException e) {
e.printStackTrace();
}
} else {
Log.e("Fiend", "还没有绑定成功");
}
}
没有改什么实质性的,只是在调用服务端方法之前打印了一个Log
,方便我们之前对比时间用。
改一下服务端的代码:
IMyAidlInterface.Stub iBinder = new IMyAidlInterface.Stub() {
// 我们在aidl文件中定义的通信规则
@Override
public String getMsg() throws RemoteException {
// 5秒后再返回结果
SystemClock.sleep(5 * 1000);
return "我来自MyService";
}
};
同样没有修改多少,只是延迟5秒再返回结果。我们来看一下打印结果:
从截图可以看出,客户端确实等服务端返回后再继续执行的,所以是同步。因此,时刻记住客户端调用的时候在工作线程
调用,否则有可能阻塞主线程。那想要异步该如何做?
异步调用
想要以AIDL
方式异步调用,需要用到关键字oneway
,它可以作用在接口上也可以作用在方法上。异步方法必须返回void
。
- 异步接口
// 所有方法都是异步的
oneway interface IAsynchronousInterface {
void method1();
void method2();
}
- 异步方法
interface IAsynchronousInterface {
// 这个方法是异步执行的
oneway void method1();
void method2();
}
异步已经可以了,那结果如何返回呢?通常异步都是以回调接口的方式,在这里也是一样的。我们修改上面的之前演示的示例,增加一个回调接口,方便服务端调用客户端的方法,也就是所谓的反向调用。
增加一个回调AIDL
接口定义:
增加回调接口必须重新建立一个.aidl
结尾的文件,IMyAidlInterfaceCallback.aidl
具体内容如下:
// 用于服务端回调
interface IMyAidlInterfaceCallback {
// 结果处理
void handleResult(String result);
}
修改IMyAidlInterface.aidl
的内容:
import com.fiend.aidlexample.IMyAidlInterfaceCallback;
// 和我们平常定义一个接口语法一样
oneway interface IMyAidlInterface {
// 定义了一个方法(所谓的通信规则)
void getMsg(IMyAidlInterfaceCallback callback);
}
将getMsg()
方法的返回改为void
,并将新定义的回调接口作为参数。这里必须显示import
接口,否则编译会报错。
修改服务端部分代码:
IMyAidlInterface.Stub iBinder = new IMyAidlInterface.Stub() {
@Override
public void getMsg(IMyAidlInterfaceCallback callback) throws RemoteException {
// 5秒后再返回结果
SystemClock.sleep(5 * 1000);
// 通过回调接口返回结果
callback.handleResult("我是异步返回,我来自MyService");
}
// // 我们在aidl文件中定义的通信规则
// @Override
// public String getMsg() throws RemoteException {
// // 5秒后再返回结果
// SystemClock.sleep(5 * 1000);
// return "我来自MyService";
// }
};
注释掉的部分是我们之前的做法,现在是通过回调接口返回结果。
修改客户端部分代码:
/**
* 回调接口
*/
private IMyAidlInterfaceCallback.Stub mCallback = new IMyAidlInterfaceCallback.Stub() {
@Override
public void handleResult(String result) throws RemoteException {
Log.e("Fiend", "客户端:" + result);
}
};
public void btnClick(View view) {
if (isBound) {
try {
Log.e("Fiend", "开始调用服务端方法");
// String result = mService.getMsg();
// Log.e("Fiend", "客户端:" + result);
// 调用服务端方法,将回调接口传过去
mService.getMsg(mCallback);
Log.e("Fiend", "结束调用服务端方法");
} catch (RemoteException e) {
e.printStackTrace();
}
} else {
Log.e("Fiend", "还没有绑定成功");
}
}
增加了一个回调接口,修改了调用服务端方法,之前是调用getMsg()
并返回结果,现在是调用getMsg(mCallback)
把回调方法传过去,没有返回值。返回结果在回调接口中处理。
我们来看一下运行结果:
通过返回时间对比,可以看到,调用完远程服务方法就立刻返回了。而需要返回的数据是在5
秒后通过回调接口返回的。
至此,我们就实现了AIDL
方式的异步调用了。
AIDL支持的数据类型
AIDL
默认支持这么几种数据类型:
Java
基本数据类型,如int
、long
、boolean
等(除了short
)String
类型CharSequence
List
类型,所有List
中的元素必须是AIDL
支持的类型,如List<String>
Map
类型,所有Map
中的元素必须是AIDL
支持的类型,如Map<String, Integer>
List
和Map
的接收方类型必须为ArrayList
和HashMap
。
如果默认的类型不能满足你的需要,还可以自定义类型,自定义类型必须支持序列化,也就是实现Parcelable
接口。具体可以参考官网。
以上我们介绍的
AIDL
用法都是在同一个工程里,只是将Service
指定运行在了不同的进程中,因此我们的.aidl
文件可以只写一份,但是,如果我们的Service
是在另一个应用(apk
)中,那么另一个应用中也必须有和我们项目中相同的.aidl
文件,连包名也必须一样。
总结
Android
中实现进程间通信在高层次抽象可以很方便的使用Intent
等方式来操作,相对底层的方式我们可以使用Messenger
和AIDL
,大多数情况下我们使用Messenger
就可以达到我们想要的效果了,而且使用也比AIDL
简单,所以尽量用Messenger
,实在不行再考虑AIDL
,在介绍AIDL
的时候对于支持的数据类型并没有深入的讲解与演示,可以上官网看看。
参考文献
- 官方文档
- Android Binder by Thorsten Schreiber from Ruhr-Universität Bochum
- Deep Dive into Android IPC/Binder Framework at Android Builders Summit 2013
- Efficient Android Threading by Anders Goransson