2020-09-02

tech2022-07-05  168

 

Android中的几种IPC方式

1、使用Bundle

在Android开发中,我们通常会使用Bundle在不同的组件中传递一些数据,由于Bundle 本身已经实现了Parcelable 接口,所以它可以很方便地在进程间传输。当我们在一个进程中启动了另一个进程的Activity、Service和Receiver,我们可以将需要传输的数据放入Bundle中并通过Intent传递出去。使用示例:

 

private void startMain() { Intent intent = new Intent(FirstActivity.this, MainActivity.class); Bundle bundle = new Bundle(); bundle.putString("key", "value"); …… intent.putExtra("bundle", bundle); }

我们必须要知道,我们传输的数据必须是可序列化的,比如基本类型、实现了Serializable 或Parcelable接口的对象以及一些Android支持的特殊对象,具体可以查看Bundle 这类中的一系列put 方法。Bundle 不支持的数据类型我们无法通过它在进程间传递。

2、使用文件共享

两个进程可以通过读/写同一个文件来交换数据,也就是说A进程把数据写入到共享文件中,B进程通过读取共享文件获取A进程共享的数据,这在Android 中也是一个常见的数据共享方式。

由于Android系统是基于Linux的,使得并发读写可以没有限制地进行,甚至两个线程对同一个文件进行写操作都是可以的,尽管这样可能会使数据变得杂乱。除了交换一些文本信息,我们还可以序列化一个对象到文件系统中,之后在另一个进程中恢复这个对象,虽然两个对象内容完全相同,但是得到的对象和之前的对象本质上是两个不同的对象。

 

private static final String FILE_PATH = "对象序列化路径"; /** * 序列化对象到文件系统 */ public static void presistToFile(final Serializable serializable) { new Thread(new Runnable() { @Override public void run() { File file = new File(FILE_PATH); ObjectOutputStream objectOutputStream = null; try { objectOutputStream = new ObjectOutputStream(new FileOutputStream(file)); objectOutputStream.writeObject(serializable); } catch (IOException e) { e.printStackTrace(); } finally { if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); } /** * 从文件系统反序列化一个对象 */ public static void recoverFromFile() { new Thread(new Runnable() { @Override public void run() { Serializable serializable = null; File file = new File(FILE_PATH); ObjectInputStream objectInputStream = null; try { objectInputStream = new ObjectInputStream(new FileInputStream(file)); serializable = (Serializable) objectInputStream.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); }

通过文件共享实现进程间通信的局限性也是比较明显的:当多个线程并发读写文件,那么我们得到的数据就可能不是正确的。所以在使用这项技术的时候,我们应该尽量避免并发读写这种情况的发生,或者只在对数据同步要求不高的情况下使用文件共享来实现进程间通信,并且妥善处理并发读写问题。

3、使用Messenger

Messenger是一种轻量级的IPC方案,它的底层实现是AIDL,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就能轻松地实现数据在进程间的传递。 Messenger的使用方法很简单,它对AIDL做了封装,使得我们可以很方便地进行进程间通信。同时由于它一次处理一个请求,也就是说服务端中不存在并发的情形,所以我们不用考虑线程同步的问题。

接下来分别从服务端和客户端介绍Messenger 的使用步骤:

服务端创建一个Service 来处理客户端的连接请求: 创建一个静态内部类MessengerHandler ,接收并处理客户端消息,利用MessengerHandler 创建一个Messenger, 并在onBind 方法里返回Messenger里面的Binder对象; 同时使用msg.replyTo 的Messenger 给客户端回复一个Message,告知客户端已接收到消息。

 

public class MyService extends Service { //处理客户端发来的消息 private static class MessengerHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case Constant.MSG_FROMCLIENT: //接受客户端消息 Bundle bundle = msg.getData(); LogUtils.d("qianwei", bundle.getString("msg")); //通知客户端消息已收到(发送消息给客户端) Messenger replyTo = msg.replyTo; Message replyMessage = Message.obtain(null, Constant.MSG_FROMSERVICE); Bundle data = new Bundle(); data.putString("reply", "Hello client, message received! Thank you."); replyMessage.setData(data); try { replyTo.send(replyMessage); } catch (RemoteException e) { e.printStackTrace(); } } super.handleMessage(msg); } } private Messenger messenger = new Messenger(new MessengerHandler()); @Override public IBinder onBind(Intent intent) { //messenger将消息传递给MessengerHandler处理 return messenger.getBinder(); } }

注册Service并运行在单独进程中:

 

<service android:name=".service.MyService" android:process=":remoteservice" /> 接下来看看客户端的实现: 客户端绑定服务成功会调用ServiceConnection 接口的onServiceConnected方法,根据服务端返回的Binder 创建一个Messenger,通过这个Messenger 把Message 消息传输给服务端。 同时设置"message.replyTo = replyMessenger" 接收并处理服务端回复的消息,实现方法与服务端处理消息基本相同。

 

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bindServiceAction(); } /** * 监控Service连接状态 */ private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { LogUtils.d("onServiceConnected"); //传给服务端的Message Message message = Message.obtain(null, Constant.MSG_FROMCLIENT); Bundle bundle = new Bundle(); bundle.putString("msg", "Hello service, I'm client!"); message.setData(bundle); //接收服务端回复的消息 message.replyTo = replyMessenger; try { Messenger messenger = new Messenger(service); messenger.send(message); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { LogUtils.d("onServiceDisconnected"); } }; /** * 绑定服务端Service */ private void bindServiceAction(){ Intent intent = new Intent(MainActivity.this, MyService.class); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); } //处理服务端回复的消息*************************** private static class ServiceReplyMessengerHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case Constant.MSG_FROMSERVICE: //接受客户端回复 Bundle bundle = msg.getData(); LogUtils.d("qianwei", bundle.getString("reply")); } super.handleMessage(msg); } } private Messenger replyMessenger = new Messenger(new ServiceReplyMessengerHandler()); //********************************************* @Override protected void onDestroy() { super.onDestroy(); unbindService(serviceConnection); }

到此,准备工作已经就绪,启动MainActivity我们会看到消息内容在两个不同的进程间完成了传输。

image.png

在Messenger中传递数据必须将数据放入Message中,Messenger 和Message都实现了Parcelable 接口,因此可以进行跨进程通信。也就是说,Message 支持的数据类型就是Messenger 支持的传输类型。

在Android 2.2以前,msg.obj字段不支持跨进程传输,Android 2.2之后,也只是系统提供的实现了Parcelable 接口的对象才能通过它跨进程传输,我们自己定义的Parcelable 对象是无法通过object字段传输的,这导致object 字段的实用性大大降低,所幸我们还有Bundle 字段。

最后,献上Messenger工作原理图。

Messenger的工作原理

4、使用AIDL

上一节我们介绍了使用Messenger 实现进程间通信,我们能看到Messenger 是以串行的方式处理客户端发来的请求,如果客户端同时发送大量并发请求,使用Messenger就不太合适了。同时,Messenger主要是为了跨进程传输数据,如果想实现跨进程调用服务端方法,Messenger 无法做到,我们可以使用AIDL来实现跨进程的方法调用。同样,我们从服务端和客户端两个方面介绍AIDL的使用。

服务端创建Service监听连接请求,创建AIDL文件,在文件中声明要暴露给客户端的接口,在Service中实现这些接口。

客户端首先绑定服务端的Service,成功后将服务端返回的Binder转化为AIDL接口所属类型,然后就可以调用AIDL中的方法了。

上一篇文章Android深入理解IPC机制(二)浅谈Binder中我们已经介绍了如何使用AIDL生成Binder,感兴趣的小伙伴可以先了解了解,有助于理解接下来要介绍的内容。上面只是简单地介绍了使用AIDL进行进程间通信的流程,接下来对其中的难点和实现细节进行详细介绍。

首先,创建AIDL接口,接口里面声明两个接口方法:

 

// IBookManager.aidl package com.example.qianwei.myapplication.aidl; //Book必须要显示地import进来,哪怕是在同一个package下 import com.example.qianwei.myapplication.aidl.Book; interface IBookManager { /** * 获取图书列表 */ List<Book> getBookList(); /** * 添加图书 */ void addBook(in Book book); }

在AIDL文件中并不是所有数据类型都可以使用的,我们来看看AIDL到底支持哪些类型。

Java基本数据类型String和 CharSequenceList:只支持ArrayList,里面的每个元素必须能够被AIDL支持Map:只支持HashMap,里面的每个元素必须能够被AIDL支持,包括key和value所有实现了Parcelable 接口的对象所有的AIDL接口本身也可以在AIDL文件中使用

这些就是AIDL支持的所有数据类型,其中自定义的Parcelable 对象和AIDL对象必须显示地import进来,例如IBookManager 中使用Book类必须声明 "import com.example.qianwei.myapplication.aidl.Book;"。

我们需要注意,如果AIDL文件中使用了自定义的Parcelable 对象,那么必须创建一个与该对象同名的AIDL文件,并在其中声明它是Parcelable 类型,我们为Book类创建Book.aidl文件:

 

// Book.aidl package com.example.qianwei.myapplication.aidl; // Declare any non-default types here with import statements parcelable Book;

除此之外,AIDL中除基本类型之外的其他参数都必须指定方向:in(输入型参数)、out(输出型参数)或inout(输入输出型参数)。AIDL不支持声明静态常量,这点有别于传统接口。

远程服务端Service实现

我们创建一个BookManagerService,并在其中实现我们定义的AIDL接口:

 

public class BookManagerService extends Service { //使用支持并发读写CopyOnWriteArrayList,是因为AIDL的方法是在服务 // 端的Binder池中执行的,会有多线程同时访问的情况 private CopyOnWriteArrayList<Book> bookCopyOnWriteArrayList = new CopyOnWriteArrayList<>(); private Binder binder = new IBookManager.Stub() { @Override public List<Book> getBookList() throws RemoteException { return bookCopyOnWriteArrayList; } @Override public void addBook(Book book) throws RemoteException { bookCopyOnWriteArrayList.add(book); } }; @Override public IBinder onBind(Intent intent) { return binder; } @Override public void onCreate() { super.onCreate(); bookCopyOnWriteArrayList.add(new Book(10001, "Android开发艺术探索")); } }

接着我们在AndroidManifest.xml中注册BookManagerService ,让他运行在独立进程中。

 

<service android:name=".service.BookManagerService" android:process=":remotebookmanagerservice"/>

客户端实现

客户端要做的是在绑定远程服务成功后,将服务端返回的Binder对象转换成AIDL接口,然后使用这个接口调用服务端远程方法。为了节省篇幅,这里只展示核心代码:

 

【其它代码与上节实现类似,此处省略…】 private ServiceConnection bookManagerServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { //将服务端返回的Binder对象转换成AIDL(IBookManager)接口类型 IBookManager bookManager = IBookManager.Stub.asInterface(service); try { List<Book> bookList = bookManager.getBookList(); LogUtils.d("bookList = "+bookList.toString()); bookManager.addBook(new Book(2,"Java编程思想")); LogUtils.d("add finish, bookList = "+bookManager.getBookList().toString()); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { } };

启动MainActivity,客户端成功打印出服务端的Book信息,证明跨进程方法getBookList调用是成功的。

 

结果截图

 

到此为止,我们已经完整地使用AIDL进行一次跨进程通信。

假设用户提出一个新的需求:要求有新书增加的时候服务端自动通知用户,而不需要用户自己去主动获取图书信息。这种情形在我们的日常开发中很常见,我们很容易想到观察者模式,接下来我们就试着简单实现一下这个需求。

首先我们新建一个IBookArrivedListener.aidl 用于监听"新增新书",用户通过注册这个接口来申请新书提醒功能。

 

// IBookArrivedListener.aidl package com.example.qianwei.myapplication.aidl; // Declare any non-default types here with import statements import com.example.qianwei.myapplication.aidl.Book; interface IBookArrivedListener { void onBookArrive(in Book book); }

接着在IBookManager 中新增注册与解除注册方法,并且在服务端实现这两个方法:

 

// IBookManager.aidl package com.example.qianwei.myapplication.aidl; import com.example.qianwei.myapplication.aidl.Book; import com.example.qianwei.myapplication.aidl.IBookArrivedListener; interface IBookManager { /** * 获取图书列表 */ List<Book> getBookList(); /** * 添加图书 */ void addBook(in Book book); /** * 注册监听事件 */ void registerListener(IBookArrivedListener listener); /** * 反注册监听事件 */ void unRegisterListener(IBookArrivedListener listener); }

 

private Binder binder = new IBookManager.Stub() { @Override public List<Book> getBookList() throws RemoteException { return bookCopyOnWriteArrayList; } @Override public void addBook(Book book) throws RemoteException { bookCopyOnWriteArrayList.add(book); for (IBookArrivedListener bookArrivedListener : bookArrivedListenerCopyOnWriteArrayList) { try { //notify registered listener LogUtils.d("notify registered listener:"); bookArrivedListener.onBookArrive(book); } catch (RemoteException e) { e.printStackTrace(); } } } /** * 注册监听事件 * * @param listener */ @Override public void registerListener(IBookArrivedListener listener) throws RemoteException { if (!bookArrivedListenerCopyOnWriteArrayList.contains(listener)) { bookArrivedListenerCopyOnWriteArrayList.add(listener); } else { LogUtils.d("Listener has already registered!"); } LogUtils.d("Listener list size = "+bookArrivedListenerCopyOnWriteArrayList.size()); } /** * 反注册监听事件 * * @param listener */ @Override public void unRegisterListener(IBookArrivedListener listener) throws RemoteException { if (!bookArrivedListenerCopyOnWriteArrayList.contains(listener)) { LogUtils.d("unregister listener failed!"); } else { bookArrivedListenerCopyOnWriteArrayList.remove(listener); } } };

最后我们在客户端上自定义一个IBookArrivedListener 接口,并且将它注册到服务端接口。

 

private IBookArrivedListener bookArrivedListener = new IBookArrivedListener.Stub() { @Override public void onBookArrive(Book book) throws RemoteException { LogUtils.d("New book arrived: "+book.toString()); } }; //####################################### //注册bookArrivedListener bookManager.registerListener(bookArrivedListener); //反注册bookArrivedListener if (bookManager != null) { try { bookManager.unRegisterListener(bookArrivedListener); } catch (RemoteException e) { e.printStackTrace(); } }

我们再次运行程序,可以看到如下结果:

 

运行结果

 

正如结果显示,客户端确实接收到新书到来的通知,但是当我们退出MainActivity试图解除bookArrivedListener通知的时候,告诉我们解除失败,这显然不是我们想要的结果。

但是仔细想想,好像这种方法确实无法完成反注册功能,因为对象的跨进程传输本质上都是反序列化的过程,对象通过Binder传递到客户端后,得到的对象将会是一个全新的对象,自然也就无法完成反注册过程。

那么这种情况下我们该如何完成反注册呢?答案就是使用RemoteCallbackList,我们接下来就详细分析它的用法(暂时不介绍RemoteCallbackList的工作原理,以后会补上)。

RemoteCallbackList使用起来很方便,我们首先用它代替现有的CopyOnWriteArrayList,然后直接在相应位置调用它的register和unRegister方法,我们看看核心代码。

 

private RemoteCallbackList<IBookArrivedListener> bookArrivedListenerRemoteCallbackList = new RemoteCallbackList<>(); … //注册 bookArrivedListenerRemoteCallbackList.register(listener); … //反注册 bookArrivedListenerRemoteCallbackList.unregister(listener);

接着就是给所有注册了通知的客户端发送通知,RemoteCallbackList 的遍历方式很有意思,我们必须按照以下方式遍历RemoteCallbackList ,而且beginBroadcast和finishBroadcast必须成对使用,否则程序会出错。

 

int size = bookArrivedListenerRemoteCallbackList.beginBroadcast(); for (int i = 0; i < size; i++) { IBookArrivedListener bookArrivedListener = bookArrivedListenerRemoteCallbackList.getBroadcastItem(i); if (bookArrivedListener != null) { bookArrivedListener.onBookArrive(book); } } bookArrivedListenerRemoteCallbackList.finishBroadcast();

我们可以试试程序修改后的运行结果。

 

 

从结果来看,RemoteCallbackList的确可以完成跨进程的解注册功能。

5、使用ContentProvider

ContentProvider 是Android中提供的专门用于不同进程间数据共享的方式,ContentProvider作为Android中的四大组件之一,可见它在Android中是比较重要的。它的底层实现同样是Binder,但是它的使用过程比AIDL方便得多,因为系统为我们做了封装,使得我们不需要关心底层细节就可以轻松实现进程间通信。

系统也为开发者提供了很多内置的ContentProvider ,例如通讯录、相册信息等,要跨进程访问这些数据,我们就必须先了解ContentProvider 的创建以及使用规则。

 

 

最新回复(0)