一.Android中的IPC方式
本节我们开始详细的分析各中跨进程的方式,具体方式有很多,比如可以通过在Intent中附加extras来传递消息,或者通过共享文件的方式来共享数据,还可以采用Binder方式来跨进程通信,另外,ContentProvider天生就是支持跨进程访问的,所以通过Socket也可以实现IPC,上述的各种方法都能实现IPC,他们的使用方式和侧重点上有很大的区别,我们在这里都会一一说的;
1.使用Bundler
Bundler,用Intent传值的引用对象,我们都知道A,S,B三大组件都是可以通过intent的Bundler传值的,那是因为Bundler实现了Parcelable接口,所以他可以在不同的进程间传输,基于这一点,当我们在一个进程中使用另外一个进程的A,S,B,我们就可以在Bunlder中附加我们需要传输给远程进程的信息,然后用intent发送过去,当然,我们传输的数据必须能够序列化,比如基本数据类型,实现了Parcelable接口的对象,实现了Serializable接口的对象以及一些Android支持的特殊对象,具体内容可以看下Bundler这个类,就可以看出他所支持的类型了,Bundler不支持的类型我们无法通过他在进程间传递数据,这个很简单,就不再详细介绍了,这是一种很简单的进程间通信方式
除了直接传递数据这种典型的使用场景,他还有一种特殊的使用场景,比如A进程正在进行一个计算,计算完成之后他要启动B进程一个组件并把计算结果传递给B进程,可是遗憾的是这个数据不支持放在Bundler里,因此无法使用intent来传递,这个时候如果我们用其他IPC,那就稍微有些麻烦了,可以考虑如下形式:通过intent启动进程B的一个Service组件,让Service去完成计算,这样就不再需要什么跨进程了,这也只是一种小的技巧
2.使用文件共享
文件共享是一种不错的进程间通讯的方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B再去读取,我们知道,在Winddows上,一个文件如果被加了排斥锁将会导致其他线程无法对其访问,甚至两个线程同时进度写的操作也是不允许的,尽管这可能出问题,通过文件交换数据很好使用,除了可以交换一些文本信息外,我们还可以序列化UI和对象到文件里,引用的时候再恢复,下面就来展示下这个功能
还是本章的例子,在onResume序列化一个对象到sd卡,第二个Activity去恢复,,关键代码如下:
MainActivity
private void persistToFile() {
new Thread(new Runnable() {
@Override
public void run() {
User user = new User("1", "jek", false);
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdir();
}
File cachedFile = new File(PATH);
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(new FileOutputStream(cachedFile));
objectOutputStream.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
SecondActivity
private void recoverFromFile() {
new Thread(new Runnable() {
@Override
public void run() {
User user = null;
File cachedFile = new File(MainActivity.PATH);
if(cachedFile.exists()){
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(cachedFile));
user = (User) objectInputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}).start();
}
如果我们查看log的话会发现,,是成功的恢复了User对象的数据的
通过文件共享这种方式来共享数据对文件格式是没有具体要求的,比如可以是文本文件,也可以是XML文件,只要读/写双方约定数据格式即可。通过文件共享的方式也是有局限性的,比如并发读/写的问题,像上面的那个例子,如果并发读/写,那么我们读出的内容就有可能不是最新的,如果是并发写的话那就更严重了。因此我们要尽量避免并发写这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。通过上面的分析,我们可以知道,文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。
当然,SharedPreferences是个特例,众所周知,SharedPreferences是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,在底层实现上它采用XML文件来存储键值对,每个应用的SharedPreferences文件都可以在当前包所在的data目录下查看到,一般来说,它的目录位于/data/data/package name/shared_prefs目录下,其中package name表示的是当前应用的包名。从本质上来说,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问Sharedpreferences有很大几率会丢失数据,因此,不建议在进程间通信中使SharedPreferences。
3.使用Messenger
Messenger可以翻译为信使,顾名思义,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL,为什么这么说呢,我们大致看一下Messenger这个类的构造方法就明白了。下面是Messenger的两个构造方法,从构造方法的实现上我们可以明显看出AIDL的痕迹,不管是IMessenger还是Stub.asInterface,这种使用方法都表明它的底层是AID
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public Messenger(IBinder target){
mTarget = IMessenger.Stud.asInterface(target);
}
Messenger的使用方法很简单,它对AIDL做了封装,使得我们可以更简便地进行进程通信。同时,由于它一次处理一个请求,因此在服务端我们不用考虑线程同步的问题,这是因为服务端中不存在并发执行的情形。实现一个Messenger有如下几个步骤,分为服
务端和客户端。
1.服务端进程
首先,我们需要在服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder即可。
2.客户端进程
客户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发消息类型为
Message对象。如果需要服务端能够回应客户端,就和服务端一样,我们还需要创建一个Handdler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端。这听起来可能还是有点抽
象,不过看了下面的两个例子,读者肯定就都明白了。首先,我们来看一个简单点的例子,
这个例子中服务端无法回应客户端。。首先看服务端的代码,这是服务端的典型代码,可以看到MessengerHandler用来处理客户端发送的消息,并从消息中取出客户端发来的文本信息,而mMessenger是一个Mwsswnger对象,他和MessengerHandler相关联,并在onBind方法中返回他里面的IBind对象
public class MessengerService extends Service {
public static final String TAG = "MessengerService";
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 100:
Log.i(TAG,"数据:" + msg.getData());
break;
default:
super.handleMessage(msg);
}
}
}
private final Messenger mMessenger = new Messenger(new MessengerHandler());
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
然后注册下这个Service,让其在独立进程中运行
接下来我们看看服务端,服务端比较简单
public class MessengerActivity extends AppCompatActivity {
public static final String TAG = "MessengerActivity";
private Messenger mService;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new Messenger(service);
Message msg = Message.obtain(null,10);
Bundle data = new Bundle();
data.putString("msg","hell this is client");
msg.setData(data);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_messenger);
Intent intent = new Intent(this,MessengerService.class);
bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(mConnection);
}
}
这样运行之后就能收到发送的消息了
通过上面的例子可以看出,在Messenger中进行数据传递必须将数据放入Messsage中,而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输,简单来说,Messebger所支持的数据类型就是Message所支持的传输类型。实际上,通过 Messenger来传递Message,Message中能使用的载体就只有what、arg1、arg2、Bundle以及replyTo。Message的另一个字段object在同一个进程中是很实用的,但是在进程间通信的时候,在Android2.2以前object字段不支持跨进程传输,即便是2.2以后,也仅仅是系统提供的实现了Parcelable接口的对象才能通过它来传输。这就意味着我们自定义的Parcelable对象是无法通过object字段来传输的,读者可以试一下。非系统的Parcelable对象的确无法通过object字段来传输,这也导致了object字段的实用性大大降低,所幸我们还有Bundle,Bundle中
可以支持大量的数据类型。
上面的例子演示了如何在服务端接收客户端中发送的消息,但是有时候我们还需要能回应客户端,下面就介绍如何实现这种效果。还是采用上面的例子,但是稍微做一下修改,每当客户端发来一条消息,服务端就会自动回复一条“嗯,你的消息我已经收到,稍后
回复你。”,这很类似邮箱的自动回复功能。
首先看服务端的修改,服务端只需要修改MessengerHandler,当收到消息后,会立即回复一条消息给客户端
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 100:
Log.i(TAG,"数据:" + msg.getData());
Messenger messenger = msg.replyTo;
Message reply = Message.obtain(null,200);
Bundle bundle = new Bundle();
bundle.putString("reply","收到你的消息,等下回复");
try {
messenger.send(reply);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
接着我们来看看客户端的修改,为了接受服务端的恢复,客户端也需要准备一个接收消息的Messenger和handler,如下:
private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 200:
Log.i(TAG,"Service:" + msg.getData().getString("reply"));
break;
default:
super.handleMessage(msg);
}
}
}
除了上述的修改,还有关键的一点,当客户端发送消息的时候,需要把接收服务端回复的Messenger通过Message的replyTo参数传递给服务端,如下:
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new android.os.Messenger(service);
Message msg = Message.obtain(null,100);
Bundle data = new Bundle();
data.putString("msg","hell this is client");
//注意这句话
msg.replyTo = mGetReplyMessenger;
msg.setData(data);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
通过上述的修改,我们再次运行,就达到了自动回复的效果了;
到这里,我们采用Messenger进程通讯的例子就说完了,我们画一张工作原理图,这样更加便于理解:
关于进程间通信,这个只能针对于同一个应用的通讯,那有没有针对不同应用的?是这样的,之所以选择在同一个应用内进行进程间通信,是因为操作起来比较方便,但是效果和在两个应用间进行进程间通信是一样的。在本章刚开始就说过,同一个应用的不同组件,如果它们运行在不同进程中,那么和它们分别属于两个应用没有本质区别,关于这点需要深刻理解,因为这是理解进程间通信的基础。
四.使用AIDL
上一节我们介绍了使用Messenger来进行进程间通信的方法,可以发现,Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是我们可以使用AIDL来实现跨进程的方法调用。AIDL是Messenger的底层实现,因此Messenger本质上也是AIDL,只不过系统为我们做了封装,从而方便上层的调用而已。在上一节中,我们介绍了Binder的概念,大家对Binder也有了一定的了解,在Binder的基础上我们可以更加容易地理解AIDL。这里先介绍使用AIDL
来进行进程间通信的流程,分为服务端和客户端两个方面。
1.服务端
服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可
2.客户端
客户端所要做事情就稍微简单一些,首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了
上面描述的只是一个感性的过程,AIDL的实现过程远不止这么简单,接下来会对其中的细节和难点进行详细介绍,并完善我们在Binder那一节所提供的的实例。
3.AIDL接口的创建
首先看AIDL接口的创建,如下所示,我们创建了一个后缀为AIDL.的文件,在里面明了一个接口和两个接口方法。