[Android]《Android开发艺术探索》学习笔记-IPC机制

一、Android IPC简介

IPC是Inter-Process Communication的缩写,意思是进程间通信或跨进程通信,是指两个进程之间进行数据交换的过程。Android最具有特色的跨进程通信就是Binder了,然后还有Socket、ContentProvider等。

Android中IPC的使用场景:有些模块由于特殊原因需要运行在单独的进程中;通过多进程来获取多份内存空间;当前应用需要向其他应用获取数据。

 

二、Android中的多进程模式

1、开启多进程模式

给四大组件在Manifest中指定android:process属性。这个属性的值就是进程名。

进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以跑在同一个进程中,而进程名不以“:”开头代表全局进程,其他应用通过ShareUID方式可以和他跑在同一个进程中。

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

Android会为每一个应用分配一个独立的虚拟机,或者说为每一个进程都分配了独立的虚拟机,不同的虚拟机在内存分配上会有不同的地址空间,多台虚拟机访问同一个类的对象会产生不同的副本。比如不同进程的Activity对静态变量的修改,对其他进程不会造成任何影响。正常情况下,四大组件中间不可能不通过一些中间层来共享数据,如果用内存来共享数据注定是失败的。

一般来说,使用多进程会造成以下问题:(1)静态成员和单例模式完全失效。(2)线程同步机制完全失效。(3)SharePreferences的可靠性下降。(4)Application会多次创建。

问题(1)上面已分析,问题(2)是因为不是一块内存了,不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象。问题(3)是因为SharePreferences不支持两个进程同时去执行写操作,可能会导致一定几率的数据丢失。问题(4)是因为Android中每个进程独享一个虚拟机,这样相当于单独打开一个APP。

在多进程模式中,不同进程的组件拥有独立的虚拟机、Application以及内存空间。 实现跨进程的方式有很多:Intent传递数据、共享文件和SharedPreferences、基于Binder的Messenger和AIDL、Socket。

 

三、IPC基础概念介绍

1、Serializable接口

Serializable是Java提供的一个序列化接口(空接口),只需要实现这个接口并在类的声明中指定一个serialVersionUID即可自动实现默认的序列化过程。

serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化,如果不同程序就会crash。

静态成员变量属于类不属于对象,不参与序列化过程。transient关键字标记的成员变量不参与序列化过程。

2、Parcelable接口

Parcel内部包装了可序列化的数据,可以在Binder中自由传输。序列化功能由writeToParcel方法完成,最终是通过Parcel的一系列writer方法来完成;反序列化功能由CREATOR来完成,其内部表明了如何创建序列化对象和数组,通过Parcel的一系列read方法来完成;内容描述功能由describeContents方法完成,几乎所有情况下都应该返回0,仅当当前对象中存在文件描述符时返回1。代码如下:

public class Person implements Parcelable{
      private String name;
      private Int age;

      ...//设置setter() 和 getter()方法

      //下面是实现Parcelable接口的内容
      @Override
      public int describeContents() {
          return 0;                                            //一般返回零就可以了
      }

      @Override
      public void writeToParcel(Parcel dest, int flags) {      //在这个方法中写入这个类的变量
          dest.writeString();                    //对应着 String name;
          dest.writeInt();                        //对应着 Int age;
      }
      //在实现上面的接口方法后,接下来还需要执行反序列化,定义一个变量,并重新定义其中的部分方法
      public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>(){

        @Override
        public Person createFromParcel(Parcel source) {                  //在这个方法中反序列化上面的序列化内容,最后根据反序列化得到的各个属性,得到之前试图传递的对象
             //反序列化的属性的顺序必须和之前写入的顺序一致
            Person person = new Person();
            person.name = source.readString();
            person.age = source.readAge();
            return person;
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];                     //一般返回一个数量为size的传递的类的数组就可以了        }
    };
}

Serializable是Java的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。而Parcelable是Android中的序列化方式,适合在Android平台使用,效率高但是使用麻烦。Parcelable主要在内存序列化上,Parcelable也可以将对象序列化到存储设备中或者将对象序列化后通过网络传输,但是稍显复杂,推荐使用Serializable。

3、Binder

Binder是Android中的一个类,它实现了IBinder接口。从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux没有。从AndroidFramework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager,等等)和相应ManagerService的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

Android中Binder主要用在Service中,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制:(Book.java 表示图书信息的实体类,实现了Parcelable接口;Book.aidl 表示Book类在AIDL中的声明;IBookManager.aidl 定义的管理Book实体的一个接口,包含getBookList和addBook两个方法。详细代码见《Android开发艺术探索》P48-P49,系统生成的代码见P49-P53)

在gen目录下,系统根据IBookManager.aidl为我们生成了IBookManager.java,它继承了IInterface接口,同时他自己也是个接口,所有可以在Binder传输的接口都必须继承IInterface接口

声明了getBookList和addBook方法,还声明了两个整型id分别标识这两个方法,用于标识在transact过程中客户端请求的到底是哪个方法;声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端位于同一进程时,方法调用不会走跨进程的transact。当二者位于不同进程时,方法调用需要走transact过程,这个逻辑有Stub的内部代理类Proxy来完成;这个接口的核心实现就是它的内部类Stub和Stub的内部代理类Proxy。IBookManager接口很简单,但是其核心实现就是内部类Stub和Stub的内部代理类Proxy。我们来看看Stub和Proxy类的内部方法和定义:

(1)DESCRIPTO:Binder的唯一标识,一般用Binder的类名表示。

(2)asInterface:将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,如果C/S位于同一进程,此方法返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy对象。

(3)asBinder:返回当前Binder对象。

(4)onTransact:这个方法运行在服务端的Binder线程池中,由客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为

java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)

服务端通过code确定客户端所请求的方法是什么,接着从data中取出目标方法所要的参数(有的话),然后执行目标方法。当目标方法执行完毕就向reply中写入返回值(有的话),整个onTansact的执行就是这样的。如果onTransact返回false那么客户端就请求执行失败了,所以我们可以通过这个特性来鉴权。

(4)Proxy#getBookList:这个方法运行在客户端,客户端远程调用这个方法时,内部实现如下:这个方法运行在客户端,首先该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data中(如果有);接着调用transact方法来发起RPC(远程过程调用),同时当前线程挂起;然后服务端的onTransact方法会被调用直到RPC过程返回,当前线程继续执行,并从_reply中取出RPC过程的返回结果;最后返回_reply中的数据。

(5)Proxy#addBook:与(4)一样,但其没有返回值,不用返回_reply中数据。

其实提供AIDL文件,是为了方便系统为我们生成代码,我们完全可以自己实现Binder。

我们为了知道服务端进程有没有终止,可以为Binder设置一个死亡代理:

声明一个DeathRecipient对象。DeathRecipient是一个接口,DeathRecipient只有一个方法binderDied,当Binder死亡的时候,系统就会回调DeathRecipient方法,这时我们就可以移除之前绑定的Binder代理并重新绑定远程服务:

 private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
     @Override
     public void binderDied(){
         if(mBookManager == null){
             return;
         }
         mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
         mBookManager = null;
         // TODO:这里重新绑定远程Service
     }
 }

Binder有两个很重要的方法linkToDeath和unlinkToDeath。通过linkToDeath为Binder设置一个死亡代理:

mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);//第二个参数是标记位,直接设置为0即可

当然,我们也可以用Binder的方法isBinderAlive来判断。

 

四、Android中的IPC方式

1、使用Bundle

四大组件中的三大组件(Activity、Service、Receiver)都支持在Intent中传递Bundle数据。Bundle实现了Parcelable接口,**当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。

2、使用文件共享

两个进程通过读写同一个文件来交换数据。还可以通过ObjectOutputStream/ObjectInputStream序列化一个对象到文件中,或者在另一个进程从文件中反序列这个对象。注意:反序列化得到的对象只是内容上和序列化之前的对象一样,本质是两个对象。文件并发读写会导致读出的对象可能不是最新的,并发写的话那就更严重了,所以文件共享方式适合对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题。SharedPreferences是特例,它的底层实现采用XML文件来存储键值对,所以也算是文件共享,系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时SharedPreferences有很大几率丢失数据,因此不建议在IPC中使用SharedPreferences。

3、使用Messenger

Messenger可以翻译为信使,可以通过它来传递Message对象。它的底层实现是aidl,是一种轻量级的IPC方案。实现一个Messenger有如下几个步骤,分为服务端和客户端:(详细代码见P66-P69)

(1)服务端进程

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

(2)客户端进程

绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是Message。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger(和服务端一样),并通过Message的replyTo参数传递给服务端。服务端通过Message的replyTo参数就可以回应客户端了。

客户端和服务端 拿到对方的Messenger来发送Message,只不过客户端通过bindService而服务端通过message.replyTo来获得对方的Messenger。

4、使用AIDL

如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

AIDL接口的创建:

import com.ryg.chapter_2.aidl.Book;

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
}

AIDL支持的数据类型:基本数据类型;String、CharSequence;List:只支持ArrayList,里面的每个元素必须被AIDL支持;Map:只支持HashMap,里面的每个元素必须被AIDL支持;Parcelable;所有的AIDL接口本身也可以在AIDL文件中使用

自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型:

package com.ryg.chapter_2.aidl;

parcelable Book;

AIDL接口中的参数除了基本类型以外都必须表明方向in/out(如void addBook(in Book book))。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,直接采用CopyOnWriteArrayList来进行自动线程同步。客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。

5、使用ContentProvider

ContentProvider是四大组件之一,其底层实现和Messenger一样是Binder。ContentProvider天生就是用来进程间通信,只需要实现一个自定义或者系统预设置的ContentProvider,通过ContentResolver的query、update、insert和delete方法即可。创建ContentProvider,只需继承ContentProvider实现onCreate、query、update、insert、getType六个抽象方法即可。除了onCreate由系统回调并运行在主线程,其他五个方法都由外界调用并运行在Binder线程池中。

6、使用Socket

Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。

 

五、Binder连接池

使用AIDL的流程是客户端在Service的onBind方法中拿到继承AIDL的Stub对象,然后客户端就可以通过这个Stub对象进行RPC。 那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。

服务端只有一个Service,我们应该把所有AIDL放在一个Service中去管理,不同业务模块之间是不能有耦合的,服务端提供一个queryBinder接口,这个借口能够根据业务模块的特征来返回响应的Binder对象给客户端,不同的业务模块拿到所需的Binder对象就可以进行RPC了。

 

六、选用合适的IPC方式

 

本章节所有代码除了查阅书,还可以到本书的作者任玉刚的github下载,本章代码地址:https://github.com/singwhatiwanna/android-art-res/tree/master/Chapter_2/src/com/ryg/chapter_2

发表评论

电子邮件地址不会被公开。 必填项已用*标注