-
使用Bundle
我们知道,四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。基于这一点,当我们在一个进程中启动了另一个进程的Activity、Service和Receiver,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送出去。当然,我们传输的数据必须能够被序列化,比如基本类型、实现了Parcellable接口的对象、实现了Serializable接口的对象以及一些Android支持的特殊对象,具体内容可以看Bundle这个类,就可以看到所有它支持的类型。这是一种最简单的进程间通信方式。
除了直接传递数据这种典型的使用场景,它还有一种特殊的使用场景。比如A进程正在进行一个计算,计算完成后它要启动B进程的一个组件并把计算结果传递给B进程,可是遗憾的是这个计算结果不支持放入Bundle中,因此无法通过Intent来传输,这个时候如果我们用其他IPC方式就会略显复杂。可以考虑如下方式:我们通过Intent启动进程B的一个Service组件(比如IntentService),让Service在后台进行计算,计算完毕后再启动B进程中真正要启动的目标组件,由于Service也运行在B进程中,所以目标组件就可以直接获取计算结果,这样一来就轻松解决了跨进程的问题。这种方式的核心思想在于将原本需要在A进程的计算任务转移到B进程的后台Service中去执行,这样就成功地避免了进程间通信问题,而且只用了很小的代价。
-
使用文件共享
共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。通过文件共享这种方式来共享数据对文件格式是没有具体要求的,比如可以是文本文件,也可以是XML文件,只要读/写双方约定数据格式即可。通过文件共享的方式也是有局限性的,比如并发读/写的问题,如果并发读/写,那么我们读出的内容就有可能不是最新的,如果是并发写的话那就更严重了。因此我们要尽量避免并发写这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。通过上面的分析,我们可以知道,文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。
从本质上来说,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,Sharedpreferences有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
-
使用Messenger
Messenger可以翻译为信使,顾名思义,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。Messenger的使用方法很简单,它对AIDL做了封装,使得我们可以更简便地进行进程间通信。同时,由于它一次处理一个请求,因此在服务端我们不用考虑线程同步的问题,这是因为服务端中不存在并发执行的情形。
在Messenger中进行数据传递必须将数据放入Message中,而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输。简单来说,Message中所支持的数据类型就是Messenger所支持的传输类型。实际上,通过Messenger来传输Message, Message中能使用的载体只有what、arg1、arg2、Bundle以及replyTo。Message中的另一个字段object在同一个进程中是很实用的,但是在进程间通信的时候,在Android 2.2以前object字段不支持跨进程传输,即便是2.2以后,也仅仅是系统提供的实现了Parcelable接口的对象才能通过它来传输。这就意味着我们自定义的Parcelable对象是无法通过object字段来传输的,读者可以试一下。非系统的Parcelable对象的确无法通过object字段来传输,这也导致了object字段的实用性大大降低,所幸我们还有Bundle, Bundle中可以支持大量的数据类型。
-
使用AIDL
这里先介绍使用AIDL来进行进程间通信的流程,分为服务端和客户端两个方面。
1.服务端服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
2.客户端客户端所要做事情就稍微简单一些,首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。具体的例子可以参考这个
在AIDL文件中,并不是所有的数据类型都是可以使用的,那么到底AIDL文件支持哪些数据类型呢?如下所示。
- 基本数据类型(int、long、char、boolean、double等);
- String和CharSequence;
- List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;
- Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;
- Parcelable:所有实现了Parcelable接口的对象;
- AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。
以上6种数据类型就是AIDL所支持的所有类型,其中自定义的Parcelable对象和AIDL对象必须要显式import进来,不管它们是否和当前的AIDL文件位于同一个包内。另外一个需要注意的地方是,如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为parcelable类型。除此之外,AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout, in表示输入型参数,out表示输出型参数,inout表示输入输出型参数,至于它们具体的区别,这个就不说了。我们要根据实际需要去指定参数类型,不能一概使用out或者inout,因为这在底层实现是有开销的。最后,AIDL接口中只支持方法,不支持声明静态常量,这一点区别于传统的接口。
通过例子我们知道客户端客户端绑定服务端的Service,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。但是服务端要调用客户端的方法要怎么样实现呢?举一个图书馆的例子,用户不想时不时查询有没有新书,要图书馆有新书时就通知他,就算典型的观察者模式。
首先,我们需要提供一个AIDL接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能,当然用户也可以随时取消这种提醒。之所以选择AIDL接口而不是普通接口,是因为AIDL中无法使用普通接口。这里我们创建一个IOnNewBookArrivedListener.aidl文件,我们所期望的情况是:当服务端有新书到来时,就会通知每一个已经申请提醒功能的用户。从程序上来说就是调用所有IOnNew BookArrivedListener对象中的onNewBookArrived方法,并把新书的对象通过参数传递给客户端,内容如下所示。
package com.zzr.aidldemo2;
import com.zzr.aidldemo2.Book;
interface IOnNewBookArrivedListener{
void onNewBookArrived(in Book newBook);
}
除了要新加一个AIDL接口,还需要在原有的接口中添加两个新方法,代码如下所示。
// IBookManager.aidl
package com.zzr.aidldemo2;
import com.zzr.aidldemo2.Book;
import com.zzr.aidldemo2.IOnNewBookArrivedListener;
// Declare any non-default types here with import statements
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unRegisterListener(IOnNewBookArrivedListener listener);
}
服务端中Service的实现也要稍微修改一下,主要是Service中IBookManager.Stub的实现,因为我们在IBookManager新加了两个方法,所以在IBookManager.Stub中也要实现这两个方法。同时,在BookManagerService中还开启了一个线程,每隔5s就向书库中增加一本新书并通知所有感兴趣的用户。
最后,我们还需要修改一下客户端的代码,主要有两方面:首先客户端要注册IOnNewBookArrivedListener到远程服务端,这样当有新书时服务端才能通知当前客户端,同时我们要在Activity退出时解除这个注册;另一方面,当有新书时,服务端会回调客户端的IOnNewBookArrivedListener对象中的onNewBookArrived方法,但是这个方法是在客户端的Binder线程池中执行的,因此,为了便于进行UI操作,我们需要有一个Handler可以将其切换到客户端的主线程中去执行,这个原理在Binder中已经做了分析,这里就不多说了。客户端的代码修改如下:
//向服务端注册
bookManager.registerListener(listener);
private IOnNewBookArrivedListener listener = new IOnNewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book newBook) throws RemoteException {
//收到服务端的通知 切换到主线程
mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVERD, newBook).sendToTarget();
}
};
要注册就要解注册,正常情况下我们在Activity的onDestory方法调用服务端的unRegisterListener方法即可,但是会失败,因为在多进程中无法奏效,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们在注册和解注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后,却会产生两个全新的对象。因为对象是通过序列化后才能在进程间传输的。要实现注册解注册功能要用到 RemoteCallbackList,使用起来也很简单:
private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
mListenerList.register(listener);
@Override
public void unRegisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
mListenerList.unregister(listener);
private void onNewBookArrived(Book newBook) throws RemoteException {
books.add(newBook);
Log.i(TAG, "onNewBookArrived,notify listeners");
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
if (listener != null) {
listener.onNewBookArrived(newBook);
}
}
mListenerList.finishBroadcast();
}
使用RemoteCallbackList,有一点需要注意,我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List。遍历RemoteCallbackList,必须要按照上面的方式进行,其中beginBroadcast和beginBroadcast必须要配对使用,哪怕我们仅仅是想要获取RemoteCallbackList中的元素个数,这是必须要注意的地方。
注意事项:客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间地阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR。因此,如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的UI线程中去访问远程方法;同理,当远程服务端需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的线程池。所以,我们同样不可以在服务端中调用客户端的耗时方法。如果客户端的方法比较耗时的话,那么请确保服务端的调用客户端的方法运行在非UI线程中,否则将导致服务端无法响应。
如何在AIDL中使用权限验证功能。默认情况下,我们的远程服务任何人都可以连接,但这应该不是我们愿意看到的,所以我们必须给服务加入权限验证功能,权限验证失败则无法调用服务中的方法。在AIDL中进行权限验证,这里介绍两种常用的方法。第一种方法,我们可以在onBind中进行验证,验证不通过就直接返回null,这样验证失败的客户端直接无法绑定服务,至于验证方式可以有多种,比如使用permission验证。使用这种验证方式,我们要先在AndroidMenifest中声明所需的权限,比如:
<permission
android:name="com.zzr.aidldemo2.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal" />
如果我们自己内部的应用想绑定到我们的服务中,只需要在它的AndroidMenifest文件中采用如下方式使用permission即可。
<uses-permission android:name="com.zzr.aidldemo2.ACCESS_BOOK_SERVICE" />
第二种方法,我们可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。至于具体的验证方式有很多,可以采用permission验证,具体实现方式和第一种方法一样。还可以采用Uid和Pid来做验证,通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid,通过这两个参数我们可以做一些验证工作,比如验证包名。但是这种方法适合在不用aidl,自己实现binder的情况下,因为用aidl的方式onTransact方法是在系统为我们生产的java文件中的,我们无法修改。请参考这个例子。
-
使用ContentProvider
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,ContentProvider的底层实现同样也是Binder。系统预置了许多ContentProvider,比如通讯录信息、日程表信息等,要跨进程访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。创建一个自定义的ContentProvider很简单,只需要继承ContentProvider类并实现六个抽象方法即可:onCreate、query、update、insert、delete和getType。这六个抽象方法都很好理解,onCreate代表ContentProvider的创建,一般来说我们需要做一些初始化工作;getType用来返回一个Uri请求所对应的MIME类型(媒体类型),比如图片、视频等,这个媒体类型还是有点复杂的,如果我们的应用不关注这个选项,可以直接在这个方法中返回null或者“/”;剩下的四个方法对应于CRUD操作,即实现对数据表的增删改查功能。根据Binder的工作原理,我们知道这六个方法均运行在ContentProvider的进程中,除了onCreate由系统回调并运行在主线程里,其他五个方法均由外界回调并运行在Binder线程池中。
ContentProvider主要以表格的形式来组织数据,并且可以包含多个表,对于每个表格来说,它们都具有行和列的层次性,行往往对应一条记录,而列对应一条记录中的一个字段,这点和数据库很类似。除了表格的形式,ContentProvider还支持文件数据,比如图片、视频等。文件数据和表格数据的结构不同,因此处理这类数据时可以在ContentProvider中返回文件的句柄给外界从而让文件来访问ContentProvider中的文件信息。Android系统所提供的MediaStore功能就是文件类型的ContentProvider,详细实现可以参考MediaStore。另外,虽然ContentProvider的底层数据看起来很像一个SQLite数据库,但是ContentProvider对底层的数据存储方式没有任何要求,我们既可以使用SQLite数据库,也可以使用普通的文件,甚至可以采用内存中的一个对象来进行数据的存储。
android:authorities是Content-Provider的唯一标识,通过这个属性外部应用就可以访问我们的BookProvider,因此,android:authorities必须是唯一的,
-
使用Socket
Socket也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。在性能上,UDP具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。
我们介绍了各种各样的IPC方式,那么到底它们有什么不同呢?我们到底该使用哪一种呢?具体内容如下表所示,可以明确地看出不同IPC方式的优缺点和适用场景,那么在实际的开发中,只要我们选择合适的IPC方式就可以轻松完成多进程的开发场景。
