概述
IPC方式,即Android中的跨进程通信方式,主要有讲了:Bundle、文件共享、AIDL、Messenger、ContentProvider、Socket
使用Bundle(最简单的进程间通信方式)
四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。
基于这一点,当我们在一个进程中启动了另一个进程的Activity、Service和Receiver,我们可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送出去。当然,我们传输的数据必须是能够被序列化的,比如基本类型,实现了Parcelable接口的对象、实现了Serializable接口的对象以及一些Android支持的特殊对象,具体内容可以看Bundle这个类,就可以看到它所有支持的类型。
除了直接传递数据外,还有一种特殊情况
比如A进程正在进行一个计算,计算完成后它要启动B进程的一个组件并把计算结果传递给B进程,可是遗骸的是这个计算结果不支持放入Bundle中,因此无法通过Intent来传输,这个时候如果我们用其他IPC方式就会略显复杂。可以kaolv如下方式,我们通过Intent启动B进程的一个Service组件(比如IntentService),让Service在后台进行计算,计算完毕后再启动B进程中真正要启动的目标组件,由于Service也运行在B进程中,所以目标组件就可以直接获取计算结果,这样一来就轻松解决了跨进程的问题。这种方式的核心思想在于将原本需要在A进程的计算任务转移到B进程的后台Service中去执行,这样就成功避免了进程间通信问题,而且只用了很小的代价。
使用文件共享
共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据
Android系统是基于Linux的,使得其并发读/写文件可以没有限制地进行,甚至两个线程同时对一个文件进行写操作都是允许的,尽管这可能chuwenti。通过文件交换数据很好使用,除了可以交换一些文本信息外,我们还可以序列化一个对象到文本系统中的同时从另一个进程中恢复这个对象,下面就展示这种使用方法。
栗子:
在MainActivity的onResume中序列化一个User对象到sd卡上的一个文件里,然后在SecondActivity的onResume中去反序列化。关键代码如下:
MainActivity
SecondActivity
通过文件共享的方式来共享数据对文件格式是没有具体要求的,比如可以是文本文件,也可以是XML文件,只要读/写双方约定数据格式即可。但也有局限性,比如并发读/写的问题,像上面的例子,如果并发读/写,那么我们读出的内容有可能不是最新的,如果是并发写的话那就更加严重。因此我们要尽量避免并发写这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。
文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题
SharedPreferences是个特例,众所周知,sharedPreferences是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,在底层实现上它采用XML文件来存储键值对,每个应用的sharedPreferences文件都可以在当前包所在的data目录下查看到(一般来说,它的目录位于/data/data/package name/shared_prefs目录下,其中package name表示当前应用的包名)。从本质上来讲,Sharedpreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharePreferences文件的缓存,因此在多进程模式下,系统对他的读/写就变得不可靠了,当面对高并发的读/写访问,SharedPreferences有很大的几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
使用Messenger
Messenger可以翻译为信使,顾名思义,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。
从Messenger的构造方法的实现上可以看出AIDL的痕迹,不管是IMessenger还是Stub.asInterface,这种使用方法都表明它的底层是AIDL
Messenger是对AIDL做了封装,使得我们可以更简单地进行进程间通信,由于Messenger进行跨进程通信时请求队列是同步进行的,无法并发执行,一次处理一个请求,因此在服务端我们不用考虑线程同步的问题,这是因为服务端中不存在并发执行的情况
实现一个Messenger有如下步骤,分为服务端和客户端:
- 服务端进程
首先,我们需要在服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder即可。 - 客户端进程
客户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发送消息类型为Message对象。如果需要服务端能够回应客户端,就像服务端一样,我们还需要创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端了。
例子:
服务端实现
实现一个 Service 类, 定义处理消息的 Messenger 对象, 在 OnBind 时使用 Messenger 对象的 getBinder() 方法返回 Binder 对象.
创建 Messenger 对象时, 使用一个 Handler 对象构造, 此 Handler 用于处理客户端发来的消息
如果服务端需要向客户端返回数据, 在 Message 对象中取出客户端的 Messenger 对象(Message 对象的 replyTo 参数), 向其发送消息即可
客户端实现
首先需要 bindService, 成功时在 ServiceConnection 的 onServiceConnected() 方法中, 将传入 IBinder 对象构造一个 Messenger 对象, 此 Messager 对象用于向服务端发送消息.
如果需要处理服务端返回的数据, 则还需要创建另外一个 Messenger 对象. 在向服务端发送数据时, 将 Message 对象的 replyTo 设置为该 Messenger 对象
通过上面的例子可以看出,在Messenger中进行数据传递必须将数据放入Message中而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输。简单单来说,Message中所支持的数据类型都是Messenger所支持的传输类型。实际上,通过Messenger来传输Message,Message中所能使用的载体只有what、arg1、arg2、Bundle以及repyTo。Message的另一个字段object在同一个进程中是很实用的,但是在进程间通信的时候,在Android2.2以前object字段不支持跨进程传输,即便是在2.2之后,也仅仅是系统提供的实现了的Parcelable接口的对象才能通过它来传输。这就意味着我们自定义的Parcelable对象是无法通过object对象来传输的,非系统的Parcelable对象无法通过object字段来传输。
最后给出一张Messenger的工作原理图以方便读者更好地理解Messenger
以上示例是针对同一个应用中不同进程间的通信的,但对于同一个应用中的不同组件,如果它们运行在不同进程中,那么和它们分别属于两个应用没有本质区别,关于这点需要深刻理解,因为这是理解进程间通信的基础。
使用AIDL
Messenger是串行的方式处理客户端发来的消息,如果大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的做用户主要是为了传递消息,很多时候我们需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是我们可以使用AIDL来实现跨进程的方法调用,AIDL也是Messenger的底层实现,因此,Messenger本质上也是AIDL,只不过系统为我们做了封装从而方便上层的调用而已。
服务端
服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴漏给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AID接口即可
客户端
客户端所要做的是,首先需要绑定服务端的Service绑定成功后,将服务端返回的Binder对象转化成AIDl接口所属的类型,接着就可以调用AIDL中的方法
下面为一个完整的流程栗子:
AIDL接口的创建
首先创建一个后缀为AIDL的文件,在里面声明一个接口和两个接口方法。
AIDL所支持的所有类型:
- 基本数据类型(int、long、char、boolean、double等)
- String和CharSequence
- List:只支持ArrayList,里面每个元素都必须是能够被AIDL支持
- Map:只支持HashMap,里面每个元素都必须被AIDL支持,包括key和value
- Parcelable:所有实现了Parcelable接口的对象
- AIDL:所有的AIDL接口本身也可以在AIDL文件中使用
其中自定义的Parcelable对象和AIDL对象必须要显式import进来,不管它们是否和当前的AIDL文件于同一个包内
另外一点,如果AIDL文件中使用了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
在上面的IBookManager.aidl中用到了Book这个类,所以必须创建Book.aidl,类中添加如西施内容:
注意事项:
- AIDL中除了基本数据类型,其他类型的数据必须标上方向:in、out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。
- AIDL接口中只支持方法,不支持申明静态常量,这一点区别于传统的接口。
- 为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中。AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类,如果类的完整路径不一样的话,就无法反序列化成功,程序也就无法正常运行。
接下来用观察者模式实现,当服务端有新书到来时,就会通知每一个申请提醒功能的用户,首先需要提供一个AIDL接口,AIDL中无法使用不同接口:
远程服务端Service的实现
实现过程也比较简单,注意这里使用了CopyOnWriteArrayLIst,这个支持并发读写。AIDL方法是在服务端的BInder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情况,所以我们要在AIDL方法中处理线程同步,而这里直接使用了CopyOnWriteArrayList来进行自动的线程同步。(前面提到AIDL中能够使用的List只有ArrayList,但是这里确实使用了CopyOnWriteArrayList(注意它不是继承ArrayList,和ArrayList一样实现了List接口),能正常工作是因为,AIDL中所支持的是抽象的List,但是在Binder中会按照List的规范去访问数据并最终会形成一个新的ArrayList传递给客户端。所以这里采用CopyONWriteArrayList是完全可以的,和此类型的还有ConcurrentHashMap)
同时需要注册一下Service,运行在独立的进程:
客户端的实现
当有新书时,服务端会回调客户端的IOnNewBookArrivedListener对象中的onNewBookArrived方法,但是这个方法是在客户端的Binder线程池中执行的,因此,为了便于进行UI操作,需要有一个Handler可以将其切换到客户端的主线程中执行。
然而上面的代码当解注册的时候,会无法找到之前注册的listener,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象,即注册的时候会在服务端生成一个listener,在解注册的时候,会在服务端生成一个新的listener,由于是跨进程传输,传输的本质是反序列化的过程,这就是为什么AIDL中自定义对象都必须实现Parcelable接口的原因。
RemoteCallbackList是系统专门提供的用于删除进程listener的接口,RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,这点从下面他的声明就可以看出,因为所有的AIDL接口都继承自IInterface接口
它的工作原理很简单,在它的内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型。
其中Callback中封装了真正的远程listener,当客户端注册listener的时候,它会吧这个listener的信息存入mCallback中,其中key和value分别通过下面的方式获得:
虽说多进程传输客户端的同一个对象会在服务端生成不同的对象,大司农这些新生成的对象有一个共同点,那就是它们的底层的Binder对象时同一个。当客户端解注册的时候,只要遍历服务端的listener并把它删掉即可,这就是RemoteCallbackList为我们做的事。同时,当客户端进程终止的时候,它会自动移除所有的listener,而且内部也实现了线程同步功能,所以我们不必做额外的线程同步工作。
要对BookManagerService做一点修改,首先创建一个RemoteCallbackList对象来替代之前的CopyOnWriteArrayList,如下:
然后修改registerListener和unregisterListener这两个接口的实现
最后需要修改一下onNewBookArrived方法
我们无法像操作List一样去操作RemoteCallbackList,它并不是一个List,遍历它的时候,必须要按照上面的方式去进行,其中beginBroadcast和finishBroadcast必须配对使用,哪怕我们仅仅是想取RemoteCallbackList中元素的个数。
UI线程和线程池注意问题
- 客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端的线程会被挂起,这个时候如果服务端方法执行比较耗时,而客户端是UI线程的话,可以会ANR。由于客户端的onServiceConnected和onServiceDisconnected方法都运行在UI线程,所以也不可以在它们里面直接调用服务端的耗时操作(上述例子直接调用,有点错误,仅作演示使用)。
- 由于服务端给客户端回调的方法本身运行在Binder线程池中,所以在服务端方法中切记不要开线程去进行异步任务。而当远程服务端调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的线程池,所以,也不可以在服务端中调用客户端的耗时方法。
- 由于客户端的IOnNewBookArrivedList中的onNewBookArrived方法运行在客户端的Binder线程池中,所以不能在里面去访问UI相关的内容,需切换到UI线程。
Binder意外死亡
往往是由于服务端进程意外停止,这时我们需要重新连接服务。两种方法:
一、给Binder设置DeathRecipient监听,当BInder死亡时,我们会收到binderDied方法的回调,在binderDied方法中我们可以重连远程服务。
二、在onServiceDisconnected中重连远程服务。
区别在于:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调,也就是说,在binderDied方法中我们不能访问UI,这就是区别。
给服务加入权限验证功能,验证失败则无法调用服务中的方法,在AIDL中进行权限验证,介绍两种方法:
一、在onBind中进行验证,验证不通过就直接返回null,验证失败的客户端就直接无法绑定服务,验证方法可用permisson验证
在BookManagerService的onBind方法中做权限验证,如下:
如果自己内部的应用想绑定服务,只需在AndroidMenifest文件中采用如下方式使用permission即可。
二、可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果,实现可采用permission验证,具体实现和第一种一样。
还可采用Uid和Pid来验证,通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid,通过这两个参数可以做些验证工作,比如验证包名。下面例子既验证permission又验证了包名。
AIDL的使用流程总结
首先创建一个Service和AIDL接口,接着创建一个类继承AIDL接口中的Stub类并实现Stub类的中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端就可以绑定服务端的Service,建立连接后就可以访问远程服务端的方法了
使用ContentProvider
ContentProvider是Android提供的专门用于不同应用间进行数据共享的方式,从这一点来看,它天生就适合进程间通信。和Messenger一样,ContentProvider的底层实现也是Binder。
系统预置了许多ContentProvider,比如通信录信息、日程表信息等,要跨进程访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。创建一个自定义的ContentProvider很简单,只需要继承ContentProvider类并实现六个抽象方法即可:onCreate、query、update、insert、delete和getType。onCreate代表ContentProvider的创建,做些初始化的工作;getType用来返回一个Uri请求所对应的MIME类型(媒体类型),比如图片、视频等,这个媒体类型还是有点复杂的,如果我们的应用不关注这个选项,可以直接在这个方法中返回null或者“/”;剩下的四个方法对应于CRUD操作,即实现对数据表的增删改查功能。这六个方法均运行在ContentProvider的进程中,除了onCreate由系统回调并运行在主线程里,其他五个方法均由外界回调并运行在BInder线程池中。
存储形式
ContentProvider主要以表格的形式类组织数据,并且可以包含多个表,对于每个表格来说,它们都具有行和列的层次性,行往往对应一行记录,而列对应一条记录中的一个字段,类似于数据库。除了表格的形式外,ContentProvider还支持文件数据,比如图片和视频等。虽然ContentProvider的底层数据看起来像一个SQLite数据库,但是ContentProvider对底层的数据存储方式是没有任何要求的,既可以使用SQLite数据库,也可以是用户普通文件,甚至可以采用内存中的一个对象类进行数据的存储。
栗子
BookProvider类:
注意
ContentProvider通过Uri来区分外界要访问的数据集合,在本例中支持外界对BookProvider中的book表和user表进行访问,为了知道外界要访问的是哪个表,需要为它们定义单独的Uri和Uri_Code,并将Uri和对应的Uri_Code相关联,我们苦役使用UriMatcher的addURi方法将Uri和Uri_Code关联到一起。
update、insert和delete方法会引起数据源的变化,这个时候我们需要通过ContentResolver的的notifyChange方法来通知外界当前ContentProvider中的数据已经发生变化。要观察一个ContentProvider中的数据发生变化,可以通过ContentResolver的registerContentObserver方法来注册观察者,通过unregisterContentObserver方法来解除观察者。
- query、update、insert、delete四大方法是存在多线程并发访问的,因此要做好线程同步。(由于本例采用的是SQLite并且只有一个SQLiteDatabase的连接,所以可以正确应对多线程的情况。原因是SQLiteDatabase内部对数据库的操作时有同步处理的,但是如果通过多个SQLiteDatabase对象来操作数据库就无法保证线程同步,因为SQLiteDatabase对象之间无法进行线程同步)
- 如果ContentProvider的底层数据集是一块内存的话,比如是List,在这种情况下通List的遍历,插入、删除操作就需要进行线程同步,否则就会引发并发错误。
同时要注册这个BookProvider,其中android:authorities是ContentProvider的唯一标识,通过这个属性外部应用就可以访问我们的BookProvider,因此,android:authorities必须是唯一的,建议加上包名前缀
ContentProvider的权限还可以细分为读权限和写权限,分别对应android:readPermission和android:writePermission属性,如果声明了读写权限,那么外界应用必须依次声明相应的权限才可以进行读写操作,否则外界应用会异常终止。
DbOpenHelper类:
ProviderActivity类:
使用Socket
Socket也称“套接字”,它分为流式套接字和用户数据包套接字,分别对应于网络的传输控制层中的TCP和UDP。TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过“三次握手”才能完成,为了提供稳定数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。在性能上,UDP具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。
使用Socket来进行通信,有两点需要注意:
首先需要声明权限:
其次是注意不能再主线程中访问网络。4.0以上会抛异常,而且耗时操作也不能放在主线程。
栗子
简易聊天室,在远程Service建立一个TCP服务,然后在Activity中连接TCP服务,连接上以后,就可以给服务端发消息,,服务端会随机回应客户端一句话。
服务端代码:
客户端代码:
为了确定能够连接成功,这里采用超时重连策略,每次连接失败后都会尝试建立连接,。当然为了降低重试机制的开销,我们加入了休眠机制,即每次重试的时间间隔为1000毫秒。
实际上通过Socket不仅仅能实现进程间的通信,还可以实现设备间的通信,当然前提是这些设备之间的IP地址互相可见
Binder连接池
假设情况:当有很多歌不同的业务模块都需要使用到AIDL来进行进程间的通信时,若按照AIDL的方式一个个来,则需要创建相同个数的Service,显然这是不太科学的,为应对这种情况,Binder连接池呼之欲出。
Binder连接池的工作机制是这样的:**每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的BInder对象。对于服务端来说,只需要实现一个Service就够了,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到不同的Binder对象后就可以进行远程方法调用了。由此可见,Binder连接池的主要作用就是讲每个业务模块的Binder请求统一转发到远程Service中执行,从而避免重复创建Service的过程。**
栗子
模拟多个业务模块都要使用AIDL的情况,其中ISecurityCenter.aidl接口提供加密功能:
而ICompute.aidl接口提供计算加法的功能:
下面是以上两个接口的实现:
接着为Binder连接池创建AIDL接口IBinderPool.aidl:
接着是Service的代码,比较简单
onBind方法中直接返回了BinderPool的内部类BinderPoolImpl,接下来看看BinderPool类的实现:
最后是Activity的使用了:
这里需要额外说明一点:为什么要在线程中执行?
这是因为在Binder连接池的实现中,我们通过CountDownLatch将bindService这一异步操作转换成了同步操作,这就意味着它可能是耗时的,然后就是Binder方法的调用也可能是耗时的,因此不建议放在主线程中执行。
有了BinderPool可以大大方便日常的开发工作,如果有一个新的业务模块需要添加新的AIDL,那么在它实现了自己的AIDL接口后,只需要修改BinderPoolImpl中的恶queryBinder方法,给自己添加一个新的binerCode并返回相应的Binder对象即可。