你了解android的进程间通信吗?提到进程间通信,这是android开发中很重要的一环,也是面试高频知识点,本文总结一下进程间通信的几种方式,把这块知识系统起来。
什么是进程间通信
进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。
如何开启多进程
正常情况下,在Android中多进程是指一个应用中存在多个进程的情况,因此这里不讨论两个应用之间的多进程情况。在Android中上层应用开发使用多进程唯一一种方法,那就是给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性。
...
<activity
android:name="com.ryg.chapter_2.SecondActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:process=":remote" />
<activity
android:name="com.ryg.chapter_2.ThirdActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:process="com.ryg.chapter_2.remote" />
上面的示例分别为SecondActivity和ThirdActivity指定了process属性,并且它们的属性值不同,这意味着当前应用又增加了两个新进程。
其中SecondActivity和ThirdActivity的android:process属性分别为“:remote”和“com.ryg.chapter_2.remote”,这两种方式是有区别的:
- 首先,“:”的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,对于SecondActivity来说,它完整的进程名为com.ryg.chapter_2:remote,而对于ThirdActivity中的声明方式,它是一种完整的命名方式,不会附加包名信息;
- 其次,进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每个应用分配一个唯一的UID。通常,不同的APK会具有不同的userId,因此运行时属于不同的进程中,而不同进程中的资源是不共享的,然后有些时候,我们自己公司开发了多个APK并且需要他们之间互相共享资源,那么就可以通过设置相同的shareUserId和相同的签名来实现这一目的。
多进程影响
我们知道Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
所以,所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。一般来说,使用多进程会造成如下几方面的问题:
- (1)静态成员和单例模式完全失效。
- 静态成员属于类,不属于对象
- 因为单例模式想要保证全局只有一个对象,多进程会有多个副本了,那也就不能保证全局只有一个对象了,也就失去了作用了
- (2)线程同步机制完全失效。
- (3)SharedPreferences的可靠性下降。
- (4)Application会多次创建。
IPC有哪些方式
有问题就要有解决,为了解决这些问题,系统提供了很多跨进程通信方法,虽然说不能直接地共享内存,但是通过跨进程通信我们还是可以实现数据交互。
关于Android中的进程间通信,大概可以通过以下方式进行:
- Bundle:四大组件间通信
- File:文件共享
- AIDL:Binder机制
- Messager:基于AIDL、Handler实现
- ContentProvider:应用间数据共享
- Socket:建立C/S通信模型
使用Bundle
四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输,这里说明一点ContentProvider的call方法可以使用Bundle,而在其他方法中不能使用。
另外,Bundle不支持的类型我们无法通过它在进程间传递数据,这不用说了。
使用File(文件共享)
共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。
由于Android系统基于Linux,使得其并发读/写文件可以没有限制地进行,甚至两个线程同时对同一个文件进行写操作都是允许的,那么我们读出的内容就有可能不是最新的,如果是并发写的话那就更严重了。
普通文件并发执行,如果妥善处理并发的速度和频率或许可以保证数据的准确性。然而SharedPreferences是个特例,从本质上来说,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,Sharedpreferences有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
使用Messenger
Messenger可以翻译为信使,顾名思义,通过它可以在不同进程中传递Message(信息)对象,在Messenger(信使)中进行数据传递必须将数据放入Message(信息)中,而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输。
Messenger是一种轻量级的IPC方案,它的底层实现是AIDL:
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
Messenger的使用方法很简单,它对AIDL做了封装,使得我们可以更简便地进行进程间通信。同时,由于它一次处理一个请求,因此在服务端我们不用考虑线程同步的问题,这是因为服务端中不存在并发执行的情形。
Messager一般用在同一个应用多个不同进程服务的通信处理,下边通过代码描述一下服务端和客户端两个过程Messager通信实现过程:
服务端进程:
public class MessengerService extends Service {
private static final String TAG = "MessengerService";
//继承Handler,MessengerHandler用来处理客户端发送的消息,并从消息中取出客户端发来的文本信息。
private static class MessengerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MyConstants.MSG_FROM_CLIENT:
Log.i(TAG, "receive msg from Client:" + msg.getData().getString("msg"));
/**
* 因为我们的Messenger是通过客户端来获取的,而在客户端那边这个Messenger是以Handler为参数创建的,
* 所以在服务端通过客户端的Messenger发送消息后,在客户端的Handler就会处理这条消息,
* 嘻嘻,就达到了消息传送的目的。
*/
Messenger client = msg.replyTo;
Message relpyMessage = Message.obtain(null, MyConstants.MSG_FROM_SERVICE);
Bundle bundle = new Bundle();
bundle.putString("reply", "嗯,你的消息我已经收到,稍后会回复你。");
relpyMessage.setData(bundle);
try {
client.send(relpyMessage);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
//这是我们服务端自己的Messenger,它是以上面的Handler对象为参数创建的,
//这个Messenger是要通过绑定该服务器的时候onBind方法传递给客户端,然后客户端获取了该Messenger,
//再以该Messenger来发送消息,这样服务端就可以接收到该消息并处理。
private final Messenger mMessenger = new Messenger(new MessengerHandler());
//这个方法是在绑定服务的过程中调用的并将结果返回给客户端的,所以通过onBind方法客户端就可以获取我们Messenger的Binder对象了,
//然后客户端可以根据该Binder对象来创建一个Messenger,这样客户端中用的Messenger和这里的Messenger就是向对应的了。
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
}
然后,注册service,让其运行在单独的进程中:
<service
android:name="com.ryg.chapter_2.messenger.MessengerService"
android:process=":remote" >
客户端进程:
public class MessengerActivity extends Activity {
private static final String TAG = "MessengerActivity";
private Messenger mService;
//准备一个接收消息的Messenger和Handler
//这是客户端自己的Messenger,传递给服务端,让服务端返回消息用的。
private Messenger mGetReplyMessenger = new Messenger (new MessengerHandler ());
private static class MessengerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MyConstants.MSG_FROM_SERVICE:
Log.i (TAG, "receive msg from Service:" + msg.getData ().getString ("reply"));
break;
default:
super.handleMessage (msg);
}
}
}
/**
* 这个是客户端用来绑定服务端用的,在绑定过程中会调用onServiceConnected,它的第二个参数IBinder service,
* 就是在服务端中onBind方法返回的结果,这个结果是服务端的Messenger对象的Binder对象,
* 然后客户端通过这个Binder对象就可以创建一个Messenger,
* 所以就是在绑定服务的过程中将服务端的Messenger传递给了客户端,建立起了两者之间的桥梁
*/
private ServiceConnection mConnection = new ServiceConnection () {
public void onServiceConnected(ComponentName className, IBinder service) {
mService = new Messenger (service);
Log.d (TAG, "bind service");
Message msg = Message.obtain (null, MyConstants.MSG_FROM_CLIENT);
Bundle data = new Bundle ();
data.putString ("msg", "hello, this is client.");
msg.setData (data);
//把接收服务端回复的Messenger通过Message的replyTo参数传递给服务端
msg.replyTo = mGetReplyMessenger;
try {
mService.send (msg);
} catch (RemoteException e) {
e.printStackTrace ();
}
}
public void onServiceDisconnected(ComponentName className) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_messenger);
Intent intent = new Intent ("com.ryg.MessengerService.launch");
//在bindService的时候,服务端会通过onBind方法返回一个包含了服务端业务调用的Binder对象,
//通过这个对象,客户端就可以获取服务端提供的服务或者数据,
bindService (intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService (mConnection);
super.onDestroy ();
}
}
客户端的实现也比较简单,首先需要绑定远程进程的MessengerService,绑定成功后,根据服务端返回的binder对象创建Messenger对象并使用此对象向服务端发送消息。
Messenger的工作原理图:
Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了。但是我们可以使用AIDL来实现跨进程的方法调用。
使用AIDL
AIDL,一种接口定义语言,用于约束两个进程之间的通讯规则,供编译器生成代码实现Android设备的IPC。AIDL也是Messenger的底层实现,因此Messenger本质上也是AIDL,只不过系统为我们做了封装从而方便上层的调用而已。
AIDL方式主要用于两个应用间数据传输和方法调用实现。这里简单描述一下服务端和客户端AIDL各自的工作:
- 服务端:服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可
- 客户端:客户端所要做事情就稍微简单一些,首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中处理线程同步,如果是用List存储对象,那么可更换为CopyonWriteArayList来进行自动的线程同步,下边实现一个aidl接口:
// 定义aidl接口
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
//实现aidl接口
public class BookManagerService extends Service {
private static final String TAG = "BookManagerService";
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1,"Android"));
mBookList.add(new Book(1,"IOS"));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
AIDL工作原理图:
使用ContentProvider
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,从这一点来看,它天生就适合进程间通信。和Messenger一样,ContentProvider的底层实现同样也是Binder,但是它的使用过程要比AIDL简单许多,这是因为系统已经为我们做了封装,使得我们无须关心底层细节即可轻松实现IPC。
系统预置了许多ContentProvider,比如通讯录信息、日程表信息等,要跨进程访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。
ContentProvider的使用可参看我之前的一篇文章《Android开发ContentProvider学习总结》
使用Socket
Socket也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。
- TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;
- 而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。
- 在性能上,UDP具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。
两个进程可以通过Socket来实现信息的传输,Socket本身可以支持传输任意字节流,很显然,这是一种IPC方式。
Socket的使用可参看我之前的一篇文章《Java开发揭开socket编程的面纱》
选择合适的IPC
不同IPC方式有不同适用场景,在实际的开发中,只要我们选择合适的IPC方式就可以轻松完成多进程的开发场景。
IPC方式的优缺点和适用场景表:
RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。特指一种隐藏了过程调用时实际通信细节的IPC方法。而IPC概念泛指进程之间任何形式的通信行为。
这里再对比上表总结一下:
- 只有允许不同应用的客户端用 IPC 方式调用远程方法,并且想要在服务中处理多线程时,才有必要使用
AIDL
- 如果需要调用远程方法,但不需要处理并发 IPC,就应该通过实现一个
Binder
创建接口 - 如果您想执行 IPC,但只是传递数据,不涉及方法调用,也不需要高并发,就使用
Messenger
来实现接口 - 如果需要处理一对多的进程间数据共享(主要是数据的 CRUD),就使用
ContentProvider
- 如果要实现一对多的并发实时通信,就使用
Socket