【学习总结】Android的IPC机制(下)——Android中的IPC方式

Bundle

使用Bundle

四大组件中Activity,Service,Receiver都支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以可以方便地在不同的进程间传输。基于这一点,当我们在一个进程中启动了另一个进程的三个组件时,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送。我们传输的数据必须能够被序列化,比如基本类型、实现了Parcelable接口、Serializable接口的对象以及一些安卓支持的特殊对象,具体内容可以看Bundle这个类,它不支持的类型我们无法通过它在进程间传递数据

特殊的使用场景

比如A进程正在进行一个计算,计算完成后他要启动进程B的一个组件并把计算结果传递给进程B,但是这个结果不支持放入Bundle中,因此无法通过Intent来传输,我们可以通过Intent启动进程B的一个Service,让服务在后台进行计算,计算完毕后再启动进程B真正要启动的目标组件,由于Service也运行在进程B中,所以目标组件就可以直接获取计算结果,这种方法的核心思想在于将原本需要在进程A的计算任务转移到进程B的Service中去执行,这样成功避免了进程间通信问题。

使用文件共享

两个进程通过读写同一个文件夹来交换数据,在Windows上,一个文件如果被夹了排斥锁将会导致其他线程无法对其进行访问,包括读写,而安卓基于Linux系统使得其并发读写文件可以没有限制的进行,甚至两个线程同时对同一个文件进行写操作都是允许的,尽管这可能有问题,文件除了可以交换一些文本信息外,还可以序列化一个对象到文件系统中同时从另一个进程恢复这个对象。

通过文件共享这种方式来共享数据是没有具体要求的,只要读写双方约定数据格式即可,但也有其局限性,比如并发读写的问题,文件共享方式只适合在数据同步要求不高的进程之间通信,并且要妥善处理并发读写的问题。

特例

SharedPreferences 它通过键值对的方式来存储数据,在底层实现上它采用xml来存储键值对。每个应用的SharedPreferences文件都可以在当钱包所在的data目录下查看到。一般来说,目录位于/data/data/package name/shared_prefs目录下,package name表示的是当前应用的包名。本质上来说它也是文件的一种,但系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,在面对高并发的读写时它很大概率丢失数据,不建议在进程间通信使用

Messenger

通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL,这个可以查看它的构造函数源码得出

image.png

image.png

它对AIDL进行了封装,使得我们可以更简便地进行进程间通信。因为它每次处理一个请求,因此在服务端我们不用考虑线程同步的问题,因为服务端中不存在并发执行的情况。

实现Messenger的步骤

1.在服务端创建一个Service处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder即可。

服务端代码

image.png
服务端分析:MessengerHandler用来处理客户端发送的信息,并从消息中取出客户端发来的文本信息。而mMessenger是一个Messenger对象,它和MessengerHandler相关联,并在onBind方法返回它里面的Binder对象。这里Messenger的作用是将客户端发送的消息传递给MessengerHandler处理。然后注册Service让其运行在单独的进程中

客户端代码

image.png
image.png

客户端分析:绑定远程进程的MessengerService,绑定成功后,根据服务端返回的binder对象创建Messenger对象并使用此对象向服务端发信息。如果需要服务端能够回应客户端,我们还需要创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端就可以通过这个replyTo参数就可以回应客户端。
eg: 在原来的服务端代码中的handlerMessage方法中加入以代码即可返回信息给客户端

Messenger client=msg.replyTo;  
Message replyMessage=Message replyMessage=Message.obtain(...);  
Bundle bundle=new Bundle();  
bundle.putString("reply","");  
replyMessage.setData(bundle);  
try{  
client.send(replyMessage);  
}catch (RemoteException e){  
e.printStackTrace();
    }
复制代码

要想客户端接收服务端的回复,也要准备一个接受信息的Messenger和Handler,当客户端发送消息的时候,需要把接收服务端回复的Messenger通过Message的replyTo参数传递给服务器。

image.png

image.png

通过上面例子可以看出,在Messenger中进行数据传递必须将数据放入Message中,而这两个都实现了Parcelable接口,因此可以跨进程传输。实际上,通过Messenger传输Message,Message中能使用的载体只有what,arg1,arg2,Bundle,replyTo。而Message中的另一个字段object在同一进程中是很实用的,但在Android2.2以前object不支持跨进程传输,即使时之后也仅仅是系统提供的是实现了Parcelable接口的对象才能通过它来传输,这就意味着我们自定义的Parcelable对象是无法通过object传输的。

image.png

AIDL

1.服务端
服务端要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL中声明,最后在Service中实现这个AIDL接口即可。

2.客户端
绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

3.AIDL接口的创建
创建后缀为AIDL的文件,声明接口和方法,就和我们上篇的例子一样。

AIDL支持的数据类型
1.基本数据类型
2.String和CharSequence
3.List:只支持ArrayList,里面的元素必须能够被AIDL支持
4.Map:只支持HashMap,同上
5.Parcelable:所有实现了Parcelable的接口
6.AIDL:所有AIDL接口本身也可以在AIDL文件中使用

其中子当以的Parcelable对象和AIDL对象必须要显式import进来,不管它们是否和当前的AIDL文件在同一包内。 如果AIDL文件中用到了自定义的Pareclable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。比如上篇在IBookManager.aidl中,我们用到了Book这个类,所以我们要新建一个Book.aidl并在其中声明其为Parcelable类型。
除此之外,AIDL中除了基本参数类型,其它类型的参数必须标上方向:in out inout,AIDL只支持方法,不支持声明静态变量

远程服务端Service的实现

image.png
image.png
服务端创建了一个Binder对象并在onBind中返回它,这个对象继承自上篇的IBookManager.Stub并实现了它内部的AIDL方法。注意这里采用了CopyOnWriteArrayList,它支持并发读写。之前我们提到,AIDL方法是在服务端的Binder线程池中执行的,当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中处理线程同步,而我们可以通过CopyOnWriteArrayList来进行自动的线程同步。而CopyOnWriteArrayList和ArrayList没有关系,可以正常工作的原因是AIDL所支持的时抽象的List,在Binder中会按照List的规范去访问数据并最终形成一个新的ArrayList传递给客户端。与他类似的还有ConcurrentHashMap

客户端的实现 绑定远程服务并将服务端返回的Binder对象转换成AIDL接口,通过这个接口去调用服务端的远程方法。

image.png image.png 注意bookManager,他把服务端返回的binder对象转换成AIDL接口

AIDL常见难点

1.假设有一种需求,用户想每到一本感兴趣的新书就把书的信息告诉用户,这种典型的观察者模式在实际开发中用得很多。此时可以创建一个listener的aidl文件,在原有接口中添加两个注册和取消注册listener的方法。然后客户端注册listener到服务端,并在onNewBookArived方法中发送信息给handler,让handler进行操作。
2.当我们要解除注册时,会发现listener不是同一个对象而导致解除注册失败。因为Binder会把客户端传递过来的对象重新化成一个新的对象,别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程,这就是为什么AIDL的自定义对象都必须要实现Parcelable接口的原因。解注册功能可以使用RemoteCallbackList。 RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口,它是一个泛型,支持管理任意的AIDL接口,因为所有AIDL接口都继承自IInterface接口
RemoteCallbackList的工作原理:它的内部有一个Map结构专门用来保存所有的AIDL回调,key是IBinder类型,value是Callback类型,其中callback封装了真正的远程listener,当客户端注册listener的时候,他会把这个listener的信息存入mCallbacks中。
获取key:IBinder key=listener.asBinder()
获取value:Callback value= new Callback(listener,cookie)

所以说,我们可以利用它们底层的Binder对象是同一个来解除注册。遍历服务端所有listener,找到那个和解注册listener由相同Binder对象的listener删除即可,当客户端进程终止后,它能够自动移除客户端所注册的listener。其内部还实现了线程同步的功能。把CopyOnWriteArrayList替换成RemoteCallbackList.它的遍历要使用到beginBroadcast和finishBroadcast。

客户端调用远程服务的方法

被调用的方法运行在客户端的Binder线程池中,同时客户端线程会被挂起,此时服务端方法执行比较耗时的话就会造成客户端长时间挂起,如果这个线程是ui线程的话就会导致ANR。当我==当我们明确知道某个远程方法是耗时的,就要避免用ui线程去访问。由于客户端的onServiceConnected和onServiceDisconnected都运行在ui线程中,所以不可以在它们里面直接调用耗时方法。由于服务端的方法运行在服务端的Binder线程池中,所以服务端方法本就可以执行大量耗时操作,不要在服务端方法中开线程去执行异步任务,除非你明确知道自己在干什么。避免ANR在客户端把调用放在非UI线程就可以。当远程服务端要调用客户端的listener方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的Binder线程池。我们不可以在服务端调用客户端的耗时方法,请确保服务端调用该方法时运行在非ui线程中

Binder是可能意外死亡的,往往是由于服务端进程意外停止了,这时我们需要重新连接服务

1.给Binder设置DeathRecipient监听。当Binder死亡时,我们会收到binderDied方法的回调,在binderDied方法中我们可以重连远程服务。 2.在onServiceDisconnected中重连远程服务 区别:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。也就是说在binderDied中我们不能访问UI。

如何在AIDL中使用权限验证功能

1.在onBind中验证,验证不通过返回null,可以使用permission验证,这个方法同样适用于Messenger中。
2.采用Uid和Pid来做验证,通过getCallingUid和getCallingPid,我们可以利用它们来做一些验证工作。

image.png
上图验证了包名和权限。

ContentProvider

它的底层实现也是Binder。系统预置了许多ContentProvider,比如通讯录信息、日程表信息等。要跨进程访问这些信息只需要通过ContentResolver的query、update、insert、delete方法即可。而创建一个自定义的ContentProvider也很简单,继承ContentProvider并实现它的六个抽象方法即可。
1.onCreate
2.query
3.update 4.insert
5.delete
6.getType

1.代表onCreate的创建,做初始化操作
6.用来返回一个Uri请求所对应的MIME类型,比如图片、视频等,如果我们的应用不关注这个选项,可以直接在这个方法中返回null或者"/"

根据Binder的工作原理,我们知道这六个方法都运行在ContentProvider的进程中。除了onCreate由系统回调并运行在主线程里,其他五个方法均由外界回调并运行在Binder线程池中。ContentProvider主要以表格的形式来组织数据,并且可以包含多个表,对于每个表格来说,它们都具有行和列的层次性,行往往对应一条记录,而列对应一条记录中的一个字段,这点和数据库很类似。除了表格的形式,ContentProvider还支持文件数据,比如图片、视频。文件数据和表格数据的结构不同,因此处理这类数据时可以在ContentProvider中返回文件的句柄给外界从而让文件来访问ContentProvider中的文件信息。系统所提供的MediaStore功能就是文件类型的ContentProvider,详细实现可以参考MediaStore。虽然ContentProvider的底层数据看起来很像一个SQLite数据库,但是ContentProvider对底层的数据存储方式没有任何要求,我们既可以使用SQLite也可以使用普通的文件,甚至可以采用内存中的一个对象进行数据的存储。

注册ContentProvider时

authorities是它的唯一标识,它的权限还可以分为读权限readpermission和写权限writepermission,外界也得依次声明相应的权限才可进行相关操作,否则会异常终止。

为了知道外界要访问的是哪个表,我们需要为它们定义单独的Uri和Uri_Code,并将Uri和Uri_Code关联到一起,当外界请求访问Provider时,我们就可以根据请求的Uri来得到Uri_Code,有了Uri_Code就可以知道要访问哪个表。

image.png 由于insert update delete会引起数据源的改变,这个时候需要通过ContentProvider的notifyChange方法来通知外界当前Provider中的数据已经发生改变。可以调用registerContentObserver方法来注册观察者,unregisterContentObserver来解除观察者。

注意增删改查四大方法是存在多线程并发访问的,要做好线程同步,当通过多个SQLiteDatabase对象操作数据库就无法保证线程同步,因为对象之间无法进行线程同步

Socket

它是网络通信中的概念,分为流式套接字用户数据报套接字两种,分别对应TCP和UDP。TCP面向连接,提供稳定的双向通信功能,连接建立要通过三次握手才能完成,本身提供了超时重传机制。UDP无连接,效率更高,但不保证数据一定能够正确传输。
注意:

1.声明权限 image.png 2.不能在主线程中访问网络

服务端设计: image.png image.png image.png image.png

在onCreate方法中我们可以看出当服务启动时,会在线程中建立TCP服务。在TcpServer中监听8688端口,然后等待客户端的连接请求,当有客户端连接时,就会生成一个新的socket,通过每次新创建的Socket就可以分别和不同的客户端通信了。服务端每收到一次客户端的的消息,就会回复客户端。当客户端断开连接时,服务端也会相应的关闭对应Socket并结束通话进程。这里是通过输入流返回null判断客户端退出。

客户端设计:
image.png image.png image.png image.png

在onCreate方法中,开启了一个线程调用了connectCTPServer方法去连接服务端Socket,还采用了超时重连的策略。当和服务端连接成功后,就可以和服务端进行通信了,在连接服务端下面的代码块中通过while来读取服务端发来的信息。当客户端退出时,就退出循环终止线程

Binder连接池

回顾一下用AIDL进行通信的大致流程,先创建一个Service和一个AIDL接口,再创建一个类继承自AIDL的Stub类并实现Stub中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端绑定服务端Service,建立连接后就可以访问远程服务端了。但我们不能无限制地增加Service,我们应该减少Service的数量,将所有的AIDL放在同一个Service中去处理

做法:每个业务模块创建自己的AIDL接口并实现此接口,这时不同业务模块之间不可有耦合,然后向服务端提供自己的唯一标识和其对应的Binder。对于服务端来说,只需要一个Service就可以,服务端提供一个queryBinder接口,这个接口根据业务模块返回相应的Binder对象,不同业务模块拿到所需要的Binder对象后就可以进行远程方法调用。可见Binder线程池的作用就是将每个业务模块的Binder请求同意转发到远程Service中去执行

image.png Binder连接池的代码实现:
模拟多个业务模块使用AIDL的情况
image.png image.png 现在业务模块的AIDL接口定义和实现都已经完成了,这里没有为AIDL单独创建Service

然后为Binder连接池创建AIDL接口IBinderPool.aidl

interface IBinderPool{
IBinder queryBinder(int binderCode);
}
复制代码

然后为Binder连接池创建远程Service并实现IBinderPool,当Binder连接池连接上远程服务时,会根据不同模块的标识即binderCode返回不同的Binder对象,通过这个Binder对象所执行的操作全部发生在远程服务端。

Binder连接池的具体实现

image.png image.png image.png image.png

在需要重写的queryBinder中我们可以看到,利用不同的id返回了不用的binder给到连接池中。在Binder连接池的实现中,我们通过CountDownLatch把bindService这一异步操作变成了同步操作。注意BinderPool是一个单例实现,同一进程中只会初始化一次,我们提前初始化BinderPool就可以优化程序的体验,比如我们可以放在Application中提前对Binder进行初始化。BinderPool中有超时重连机制,当远程服务意外终止时,BinderPool会重新建立连接,如果这时业务模块中的Binder调用出现了异常,也需要手动去重新获取最新的Binder对象。如果有一个新的业务模块需要添加新的AIDL,那么在它实现了自己的AIDL接口后,只需要修改BInderPoolImpl中的queryBinder方法,给自添加一个code并返回相应的Binder对象即可

选用合适的IPC方式 image.png

自我总结:IPC机制的知识多且难度也比较高,看完了不一定就是理解了,我还需要多多实践。

猜你喜欢

转载自juejin.im/post/7073131189556477982