进程间通信方式
在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