android IPC(进程间通信)机制

来源:互联网 发布:淘宝客招代理话术 编辑:程序博客网 时间:2024/05/22 14:55

一、多进程的情况

1.       一个应用因为某些原因自身需要采用多进程模式实现。

可能是某些模块由于特殊原因需要运行在单独的线程中;或是为了增大一个应用可以使用的内存空间。android对单个应用使用的最大内存做了限制,早期一些版本是16M,不同设备有不同的大小。

2.       当前应用需要向其他应用获取数据。

二、Android中开启多进程模式

在Android中使用多进程只有一种方式,那就是给四大组件(Activity、Service、Receiver、Contentprovider)在AndroidMenifest中指定android:process属性,如下图所示。


图 1. Android组件指定所在进程的方式

        以上分别给ProcessOneActivity和ProcessTwoActivity指定process属性。假设当前的应用包名为“com.example.aidltest”,当ProcessOneActivity启动时,系统会为它创建一个单独的进程,经常名为 “com.example.aidltest:remote”;当ProcessTwoActivity启动时,系统也会为它创建一个单独的进程,进程名为“com.example.application2”。同时入口Activity是MainActivity,没有为它指定process属性,那么它运行在默认进程中,默认进程的进程名是包名。

        可以在android studio中的DDMS视图中查看进程信息,也可是用shell来查看,命令为:adb shell ps或者 adb shell ps | grep com.example.aidltest。

        以上两种方式为ProcessOneActivity和ProcessTwoActivity指定process属性。这两种方式的区别有两方面:首先“:”的含义是指要在当前的进程名前面加上当前的包名,第二种是一种完整的命名方式,不会附加包名信息;其次,进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用可以通过ShareUID方式和它跑在同一个进程中。

        Android系统为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据,两个应用通过UID跑在一块的要求是:这两个应用有相同的UID并且签名相同。

三、多进程模式的运行机制

用一个例子来说明多进程模式的运行机制。新建一个UserManager类,这个类有一个静态成员变量,如下所示:


图 2. UserManager类

       然后在MainActivity的onCreate中把sUserId 赋值为2,然后打印出这个静态变量的值之后,在启动ProcessOneActivity,在ProcessOneActivity中再打印一下sUserId的值。按照正常逻辑,静态变量是可以在所有的地方共享的,并且一处有修改,处处都会同步。但是从打印日志来看,ProcessOneActivity中打印出来的sUserId值依然是1。

        出现以上问题的原因是ProcessOneActivity运行在独立的进程中,android中每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间。这就导致在不同的虚拟机中访问同一个类的对象会产生多分副本。所以进程“com.example.aidltest:remote”和进程“com.example.aidltest”中都存在一个UserManager类,并且这两个类是互不干扰的,在一个进程中修改sUserId的值,对其他进程不会造成任何影响。

一般来说,使用多进程会造成如下几个方面的问题:

(1)静态成员和单例模式完全失效。

(2)线程同步机制完全失效。

(3)SharedPreferences的可靠性下降。

(4)Application会多次创建

第(3)个问题,SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失。这是因为SharedPreferences的底层是通过读写xml文件来实现的,并发写显然是会出现问题的,甚至是并发读/写都会出现问题。

第(4)个问题,当一个组件跑在一个新进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。

运行在不同进程中的组件是属于两个不容的虚拟机和Application的。图为在application的onCreate方法中打印当前进程的名称的结果:


图 3. Application所在的当前进程

从log可以看出,Application执行了三次onCreate,并且每次的进程名都不一样。这就证实了,在多进程模式中,不同进程的组件的确会拥有独立的虚拟机、Application以及内存空间。

四、IPC基础概念介绍

Android实现跨进程通信的方式有很多,比如通过Intent来传递数据,共享文件和SharePreferences,基于Binder的Messenger和AIDL以及Socket等。为了更好的理解各种IPC方式,介绍一些基础的概念。

1.       Serializable接口

Serializable是java所提供的一个序列化接口。如果一个类需要序列化,直接实现该接口即可。


图 4. 对象的序列化和反序列化过程

        可以在类的定义中指定一个序列化标识(并不是必须的)private static final long serialVersionUID = 79248759238475

序列化后的数据中的serialVersionUID 只有和当前类的serialVersionUID 相同才能够正常地被反序列化。

注:首先,静态成员变量属于类不属于对象,所以不会参与序列化过程;其次,用transient关键字标记的成员变量不参与序列化。

1.       Parcelable接口

       只要实现Parcelable接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。

        Serializable和Parcelable之间如何选取?

        Serializable和Parcelable都可以实现序列化并且都可以用于Intent间的数据传递。Serializable是java中的接口,使用简单但是开销很大;Parcelable是android中的序列化方式,效率很高,因此适合在android平台上使用,但是使用起来稍微麻烦;Parcelable主要用在内存序列化上。将对象序列化到存储设备中或者将对象序列化后通过网络传输使用Parcelable过程会稍显复杂,因此建议使用Serializable。

2.       Binder

       从android应用层来看,Binder是客户端和服务端进行通信的媒介,当bindSercive的时候,服务端会返回一个包含了服务端业务调用的Binder对象。通过Binder对象,客户端可以获取服务端提供的服务或者是数据。这里的服务包括普通服务和基于AIDL的服务。

android开发中,Binder主要用在Service中,其中普通server中的Binder不涉及进程间通信,所以较简单,无法触及Binder的工作机制。用于进程通信的Binder底层是基于AIDL的,这里选择用AIDL来分析Binder的工作机制。

      新建一个AIDL文件后,SDK会为我们生产AIDL所对应的Binder类。接下来,新建一个AIDL示例。

新建Book.java、Book.aidl和IBookManager.aidl文件。

Book.java代码如下:


图 5. Book类

        Book.java是一个表示图书信息的类,该类实现了Parcelable接口;Book.aidl是Book类在AIDL中的声明。IBookManager.aidl是一个接口,里面有两个方法getBookList和addBook,分别用来从远程服务获取图书列表和往图书列表中添加一本书。

       在包 com.example.aidltest下新建aidl文件,如新建Book.aidl文件,android studio会自动新建一个aidl文件夹,并在该文件夹下生成一个com.example.aidltest包,然后将Book.aidl文件放在该包下。(注:因为已经有了Book.java文件,在新建Book.aidl文件时androidstudio 会报“Interface Name must be unique”问题,我们可以先命名为其他名字,然后再将aidl文件名字修改为Book.aidl)。


图 6. aidl目录

Book.aidl文件

图 7. Book.aidl

IBookManager.aidl文件


图 8. IBookManager.aidl

虽然IBookManager.aidl和Book.java在同一个包下,但是仍要在IBookManager中导入Book类。

编译之后,在build/generated/source/aidl/debug/包名 目录下会自动生成IBookManager.aidl对应的IBookManager.java类。这个就是系统自动生成的Binder类。下面对IBookManager.java类进行分析。

1)  IBookManager.java继承了IInterface这个接口,同时它自己也是个接口,所有可以在Binder中传输的接口都需要继承IInterface接口。

2)  声明了getBookList和addBook这两个方法,这两个方法就是在IBookManager.aidl文件中声明的方法,同时还声明了两个整型的id分别用来表示这两个方法,用于标识在transact过程中客户端所请求的到底是哪个方法。

图 9. IBookManager.java中函数声明

1)  声明了一个内部类Stub,这个Stub就是一个Binder类。Stub类有一个内部代理类Proxy,当客户端和服务端不在同一个进程时,方法调用会走transact过程,这个逻辑是由Stub的内部代理类Proxy来完成的。可以知道,这个接口的核心实现就是它的内部类Stub和Stub的内部代理类Proxy,下面详细解释这两个类。

DESCRIPTOR

        Binder的唯一标识,一般用当前Binder的类名标识。

asInterface(android.os.IBinderobj)

       用于将服务端的Binder对象转化成客户端所需的AIDL接口类型对象,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。

asBinder()

      返回当前Binder对象。

onTransact

       该方法运行在服务端中的Binder线程池中,当客服端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法处理。服务端通过code可以确定客户端所请求的目标方法是什么,然后从data中取出目标方法所需要的参数,然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值。


图 10. onTransact实现

Proxy#getBookList和Proxy#addBook

        这两个方法运行在客户端,当客户端远程调用此方法时,首先创建该方法所需要的输入型Parcel对象_data、输出型parcel对象_reply和返回对象List;然后把该方法的参数信息写入_data;接着调用transact方法发起RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果。

五、android中的IPC机制

        分别介绍Android中的各种跨进程通信方式。

1.       使用Bundle

        当在一个进程中启动另一个进程的Acitivity、Service和Receiver,可以使用Bundle中附加需要传输给远程进程的信息并通过Intent发送出去。传输的数据必须能够被序列化,如基本类型、实现了Parcellable接口的兑现、实现了Serializable接口的对象以及一些Android支持的特殊对象。

2.       使用文件共享

       这种方式对文件格式没有具体要求,可以是文本文件,也可以是XML文件,只要读写双方预定数据格式即可。SharedPreference是android中提供的轻量级存储方案。

3、使用Messenger

       Messenger是一种轻量级的IPC方案,底层实现是AIDL。使用Messenger可以简单地进行进程间通信。同时,由于它一次处理一个请求,所以在服务端不用考虑线程同步的问题。实现一个Messenger有如下几个步骤,分为服务端和客户端。

(1)服务端

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

图 11. 使用Messenger通信时服务端实现

(2)客户端进程

       客户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发送消息为Message对象。同样,如果需要在服务端能够回应客户端,在服务端还需要创建一个Handler并传建一个新的Messenger,并把这个Messenger对应通过Message的replyTo参数传递给服务端,服务端通过replyTo参数就可以回应客户端。

图 12. 使用Messenger通信时客户端实现


图 13. 客户端绑定Service

以上程序的功能是:客户端向服务端发送了一句话,服务端收到这句话打印出来,然后向客服端发送已经收到的消息。

通过上面的例子可以看出,在Messenger中进行数据传递必须将数据放入Message中,而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输。Message中所支持的数据类型就是Messenger所支持的数据传输类型。

下面给出一张Messenger的工作原理图:


图 14. Messenger的工作原理图

注:Messeng服务端先创建一个Service用来监听客户端的连接请求,然后是以串行的方式处理客户端发来的消息,如果大量的消息同时发到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么Messenger就不太合适。

4、使用AIDL

首先介绍一下使用AIDL来进行进程间通信的流程,分为服务端和客户端两个方面:

(1)首先创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。

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

接下来对其中的细节和难点进行分析介绍:

(1)首先创建一个AIDL文件IBookManager,在里面声明两个接口方法。

    

图 15. IBookManager.aidl

AIDL文件中,不是所有与的数据类型都可以使用,AIDL文件支持的数据类型有:

1)     基本数据类型(int、long、char、boolean、double等)

2)     List:只支持ArrayList,里面每个元素都必须能够被AIDL支持

3)     Map:只支持HashMap,里面每个元素都必须能够被AIDL支持,包括key和value。

4)     Parcelable:所有实现了Parcelable接口的对象

5)     AIDL:所有的AIDl接口本身也可以在AIDL文件中使用。

        自定义的Parcelable对象和AIDL对象必须要显示import进来,不管他们是否和当前的AIDL文件位于同一个包内;另一个需要注意的地方是,如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型,例如Book这个类;除此之外,AIDL中出了基本类型,其他类型的参数必须标上方向:in、out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数;最后AIDL中只支持方法,不支持声明静态变量。

(2)远程服务端Service的实现

       首先创建一个Service,称为BookManagerService,代码如下:

图 16. 使用aidl服务端代码

在服务端定义了一个Binder类的对象mBinder,mBinder中实现了抽象类的getBookList和addBook方法。然后在Service的onBind方法中将mBinder返回。

(3)客户端的实现

客户端的代码如下:


图 17. 使用aidl客户端实现

在客户端通过IBookManager.Stub的asInterface方法得到IBookManager接口对象mRemoteBookManager,所以下面来看一下asInterface的方法实现。


        asInterface方法里首先进行了验空,这个很正常。第二步操作是调用了queryLocalInterface() 方法,这个方法是 IBinder 接口里面的一个方法,而这里传进来的IBinder 对象就是Service的onBind方法中返回的,从字面意思上看,意思大概是搜索本地是否有该对象,如果有的话就就返回。第三步是创建了一个对象返回,很显然,这就是我们的目标。接下来,果断看看BookManager.Stub.Proxy 类:

       看到这里,我们几乎可以确定:Proxy 类确实是我们的目标,客户端最终通过这个类与服务端进行通信。接下来看看getBooks()方法具体做了什么:


在这段代码里有几个需要说明的地方,不然容易看得云里雾里的:

  1. 关于 _data 与 _reply 对象:一般来说,我们会将方法的传参数据存入_data 中,而将方法的返回值的数据存入 _reply 中。
  2. 关于 transact() 方法:这是客户端和服务端通信的核心方法。调用这个方法之后,客户端将会挂起当前线程,等候服务端执行完相关任务后通知并接收返回的 _reply 数据流。关于这个方法的传参,这里有两点需要说明的地方:
  • 方法 ID :transact() 方法的第一个参数是一个方法 ID ,这个是客户端与服务端约定好的给方法的编码,彼此一一对应。在AIDL文件转化为 .java 文件的时候,系统将会自动给AIDL文件里面的每一个方法自动分配一个方法 ID。
  • 第四个参数:transact() 方法的第四个参数是一个 int 值,它的作用是设置进行 IPC 的模式,为 0 表示数据可以双向流通,即 _reply 流可以正常的携带数据回来,如果为 1 的话那么数据将只能单向流通,从服务端回来的 _reply 流将不携带任何数据。

注:AIDL生成的 .java 文件的这个参数均为 0。

接下来总结一下在 Proxy 类的方法里面一般的工作流程:

1)     生成 _data 和 _reply 数据流,并向 _data 中存入客户端的数据。

2)     通过 transact() 方法将它们传递给服务端,并请求服务端调用指定方法。

3)     接收 _reply 数据流,并从中取出服务端传回来的数据。

注意事项:

  1. 另外需要注意的是,当activity绑定service的时候,如果service和activity在一个进程中,那么service和activity就是在一个线程中,所以在service中不要做耗时的操作,以免主线程阻塞,出现ANR。客户端调用远程服务的方法,被调用的方法运行在服务端的binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时的话,就会导致客户端线程长时间的阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR,因此如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的UI线程中去调用。由于客户端的onServiceConnected和onServiceDisconnected方法都是在UI线程中,所以不能在这两个方法中调用service耗时方法。
  2. l如果service端和client端是在同一个进程的话,那么在两端之间传输的对象都是同一个,也就是说发出端的对象传到接收端还是它自己。如果不在同一个进程的话,那么发出端的对象传到接收端,就会创建一个一模一样的对象,接收端的对象在接收端的内存中,发送端的对象在发送端的内存中,所以这是两个对象。设想一种情况,多进程使用AIDL的情况下如果在service端维护一个列表List<Book> books。然后在client端生成一个Book的实例(书名叫book1)传给service,并加入到books中,那么按理来说此时service的books中应该有一本书,并且书名也叫book1。然后此时client再把刚刚生成的book实例传给后台,判断service中的books是否存在这本书,books.contains(book),然而会发现返回的是false,并不存在这本书。这里会觉得很奇怪,明明刚刚已经添加那本书,怎么可能拿不到。这其实是因为第一次client端将book1传给service的时候,底层创建了一个新的book2,只是book2和book1的书名一样,但其实是两个对象了。所以service的books中持有的其实是book2,然后第二次client端将book1传给service的时候,其实service拿到的是book3,然后通过book3区去books中找,当然是找不到了。那有什么办法解决呢?办法当然还是有的啦。可以重写equals方法,通过比较两本书的bookName是否一样,如果bookName一样,则说明是一本书,不一样则是不一样的书。如果在service端维护的是IInterface实现类的列表话,可以使用RemoteCallbackList<? implements IInterface> interfaces。RemoteCallbackList的内部有一个Map结构专门用来保存所有的AIDL回调。这个Map的Key是IBinder类型,value是Callback类型。

还有其他两种IPC方式,这里就不详细介绍了。

使用ContentProvider

使用Socket