Android的IPC机制和多进程开发模式

导语

对Android中的IPC机制和多进程开发模式有深入的理解。

主要内容

  • Android IPC 简介
  • Android中的多进程模式
  • IPC基础概念介绍
  • Android中的IPC方式
  • Binder连接池
  • 选用合适的IPC方式

具体内容

Android IPC 简介

IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程(例如Android的UI线程)。
任何操作系统都需要有相应的IPC机制。如Windows上的剪贴板、管道和邮槽;Linux上命名管道、共享内容、信号量等。Android中最有特色的进程间通信方式就是binder,另外还支持socket。contentProvider是Android底层实现的进程间通信。

在Android中,IPC的使用场景大概有以下:

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

Android中的多进程模式

开启多进程模式

在Android中使用多线程只有一种方法:给四大组件在Manifest中指定 android:process 属性。这个属性的值就是进程名。这意味着不能在运行时指定一个线程所在的进程。

<activity android:name=".MainActivity"
            android:process=":remote"/>
<activity android:name=".MainActivity"
            android:process="com.example.c2.remote"/>

两种进程命名方式的区别:

  • “:remote”
    “:”的含义是指在当前的进程名前面附加上当前的包名,完整的进程名为“com.example.c2.remote”。这种进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。
  • “com.example.c2.remote”
    这是一种完整的命名方式。这种进程属于全局进程,其他应用可以通过ShareUID方式和它跑在同一个进程中。

tips:使用 adb shell ps 或 adb shell ps|grep 包名 查看当前所存在的进程信息。

多线程模式的运行机制

Android为每个进程都分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,导致不同的虚拟机访问同一个类的对象会产生多份副本。例如不同进程的Activity对静态变量的修改,对其他进程不会造成任何影响。所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。四大组件之间不可能不通过中间层来共享数据。

多进程会带来以下问题:

  • 静态成员和单例模式完全失效。
  • 线程同步锁机制完全失效。
    这两点都是因为不同进程不在同一个内存空间下,锁的对象也不是同一个对象。
  • SharedPreferences的可靠性下降。
    SharedPreferences底层是 通过读/写XML文件实现的,并发读/写会导致一定几率的数据丢失。
  • Application会多次创建。
    由于系统创建新的进程的同时分配独立虚拟机,其实这就是启动一个应用的过程。在多进程模式中,不同进程的组件拥有独立的虚拟机、Application以及内存空间。

多进程相当于两个不同的应用采用了SharedUID的模式。

实现跨进程的方式有很多:

  • Intent传递数据。
  • 共享文件和SharedPreferences。
  • 基于Binder的Messenger和AIDL。
  • Socket等。

IPC基础概念介绍

主要介绍 Serializable 、 Parcelable 、 Binder 。Serializable和Parcelable接口可以完成对象的序列化过程,我们通过Intent和Binder传输数据时就需要Parcelable和Serializable。还有的时候我们需要对象持久化到存储设备上或者通过网络传输到其他客户端,也需要Serializable完成对象持久化。

Serializable接口

Serializable 是Java提供的一个序列化接口( 空接口),为对象提供标准的序列化和反序列化操作。只需要一个类去实现 Serializable 接口并声明一个 serialVersionUID 即可实现序列化。

private static final long serialVersionUID = 8711368828010083044L

serialVersionUID也可以不声明。如果不手动指定 serialVersionUID 的值,反序列化时如果当前类有所改变( 比如增删了某些成员变量) ,那么系统就会重新计算当前类的hash值并更新 serialVersionUID 。这个时候当前类的 serialVersionUID 就和序列化数据中的serialVersionUID 不一致,导致反序列化失败,程序就出现crash。

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

通过重写writeObject和readObject方法可以改变系统默认的序列化过程。

Parcelable接口

Parcel内部包装了可序列化的数据,可以在Binder中自由传输。序列化过程中需要实现的功能有序列化、反序列化和内容描述。

序列化功能由 writeToParcel 方法完成,最终是通过 Parcel 的一系列writer方法来完成。

    @Override
    public void writeToParcel(Parcel out, int flags) {
    out.writeInt(code);
    out.writeString(name);
    }

反序列化功能由 CREATOR 来完成,其内部表明了如何创建序列化对象和数组,通过 Parcel 的一系列read方法来完成。

public static final Creator<Book> CREATOR = new Creator<Book>() {
    @Override
    public Book createFromParcel(Parcel in) {
        return new Book(in);
    } 
    @Override
    public Book[] newArray(int size) {
        return new Book[size];
    }
};

protected Book(Parcel in) {
    code = in.readInt();
    name = in.readString();
}

在Book(Parcel in)方法中,如果有一个成员变量是另一个可序列化对象,在反序列化过程中需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。

 book = in.readParcelable(Thread.currentThread().getContextClassLoader());

内容描述功能由 describeContents 方法完成,几乎所有情况下都应该返回0,仅当当前对象中存在文件描述符时返回1。

public int describeContents() {
    return 0;
}

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

Binder

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

IPC-Binder

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。

图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。

Android中Binder主要用于 Service ,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制。

由系统根据AIDL文件自动生成.java文件
  1. Book.java
    表示图书信息的实体类,实现了Parcelable接口。
  2. Book.aidl
    Book类在AIDL中的声明。
  3. IBookManager.aidl
    定义的管理Book实体的一个接口,包含 getBookList 和 addBook 两个方法。尽管Book类和IBookManager位于相同的包中,但是在IBookManager仍然要导入Book类。
  4. IBookManager.java
    系统为IBookManager.aidl生产的Binder类,在 gen 目录下。

IBookManager继承了 IInterface 接口,所有在Binder中传输的接口都需要继IInterface接口。结构如下:

  • 声明了 getBookList 和 addBook 方法,还声明了两个整型id分别标识这两个方法,用于标识在 transact 过程中客户端请求的到底是哪个方法。
  • 声明了一个内部类 Stub ,这个 Stub 就是一个Binder类,当客户端和服务端位于同一进程时,方法调用不会走跨进程的 transact 。当二者位于不同进程时,方法调用需要走 transact 过程,这个逻辑有 Stub 的内部代理类 Proxy 来完成。
  • 这个接口的核心实现就是它的内部类 Stub 和 Stub 的内部代理类 Proxy 。
Stub和Proxy类的内部方法和定义
Binder的工作机制
  1. DESCRIPTOR
    Binder的唯一标识,一般用Binder的类名表示。
  2. asInterface(android.os.IBinder obj)
    将服务端的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写入返回值( 如果有返回值)。
  • 如果这个方法返回值为false,那么服务端的请求会失败,利用这个特性我们可以来做权限验证。
  1. Proxy#getBookList 和Proxy#addBook
    这两个方法运行在客户端,内部实现过程如下:
  • 首先创建该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List。
  • 然后把该方法的参数信息写入_data(如果有参数)。
  • 接着调用transact方法发起RPC(远程过程调用),同时当前线程挂起。
  • 然后服务端的onTransact方法会被调用知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。

AIDL文件不是必须的,之所以提供AIDL文件,是为了方便系统为我们生成IBookManager.java,但我们完全可以自己写一个。

linkToDeath和unlinkToDeath

如果服务端进程异常终止,我们到服务端的Binder连接断裂。但是,如果我们不知道Binder连接已经断裂,那么客户端功能会受影响。通过linkTODeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。

  1. 声明一个 DeathRecipient 对象。 DeathRecipient 是一个接口,只有一个方法 binderDied ,当Binder死亡的时候,系统就会回调 binderDied 方法,然后我们就可以重新绑定远程服务。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
    @Override
     public void binderDied(){
        if(mBookManager == null){
            return;
        } 
        mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
        mBookManager = null;
        // TODO:这里重新绑定远程Service
    }
};
  1. 在客户端绑定远程服务成功后,给binder设置死亡代理:
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
  1. 另外,可以通过Binder的 isBinderAlive 判断Binder是否死亡。

Android中的IPC方式

主要有以下方式:

  • Intent中附加extras
  • 共享文件
  • Binder
  • ContentProvider
  • Socket
使用Bundle

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

使用文件共享

我们可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。

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

Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。它对AIDL进行了封装,使得我们可以更简便的进行IPC。

Messenger的工作原理

具体使用时,分为服务端和客户端:

  1. 服务端:创建一个Service来处理客户端请求,同时创建一个Handler并通过它来创建一个
    Messenger,然后再Service的onBind中返回Messenger对象底层的Binder即可。
 private final Messenger mMessenger = new Messenger (new xxxHandler());
  1. 客户端:绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是 Message 。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger( 和服务端一样) ,并通过 Message 的 replyTo 参数传递给服务端。服务端通过Message的 replyTo 参数就可以回应客户端了。

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

Messenger中有一个 Hanlder 以串行的方式处理队列中的消息。不存在并发执行,因此我们不用考虑线程同步的问题。

使用AIDL

如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。

流程如下:

  1. 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
  2. 客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

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。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。

void addBook(in Book book);

AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步。类似的还有 ConcurrentHashMap 。

因为客户端的listener和服务端的listener不是同一个对象,所以 RemoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自 I 接口。

public class RemoteCallbackList<E extends IInterface>

它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。

客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。

权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:

  • 在onBind中验证,验证不通过返回null。
    验证方式比如permission验证,在AndroidManifest声明:
<permission
android:name="com.rgy.chapter_2.permisson.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>

Android自定义权限和使用权限:

public IBinder onBind(Intent intent){
    int check = checkCallingOrSelefPermission("com.ryq.chapter_2.permission.ACCESS_BOOK_SERVICE");
    if(check == PackageManager.PERMISSION_DENIED){
        return null;
    }
    return mBinder;
}

这种方法也适用于Messager。

  • 在onTransact中验证,验证不通过返回false。
    可以permission验证,还可以采用Uid和Pid验证。
使用ContentProvider

ContentProvider是四大组件之一,天生就是用来进程间通信。和Messenger一样,其底层实现是用Binder。

系统预置了许多ContentProvider,比如通讯录、日程表等。要RPC访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。

创建自定义的ContentProvider,只需继承ContentProvider类并实现 onCreate 、 query 、 update 、 insert 、 getType 六个抽象方法即可。getType用来返回一个Uri请求所对应的MIME类型,剩下四个方法对应于CRUD操作。这六个方法都运行在ContentProvider进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法都由外界调用并运行在Binder线程池中。

ContentProvider是通过Uri来区分外界要访问的数据集合,例如外界访问ContentProvider中的表,我们需要为它们定义单独的Uri和Uri_Code。根据Uri_Code,我们就知道要访问哪个表了。

query、update、insert、delete四大方法存在多线程并发访问,因此方法内部要做好线程同步。若采用SQLite并且只有一个SQLiteDatabase,SQLiteDatabase内部已经做了同步处理。若是多个SQLiteDatabase或是采用List作为底层数据集,就必须做线程同步。

使用Socket

Socket也称为“套接字”,分为流式套接字和用户数据报套接字两种,分别对应于TCP和UDP协议。Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。我们以一个跨进程的聊天程序来演示。

在远程Service建立一个TCP服务,然后在主界面中连接TCP服务。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。

除了采用TCP套接字,也可以用UDP套接字。实际上socket不仅能实现进程间的通信,还可以实现设备间的通信(只要设备之间的IP地址互相可见)。

Binder连接池

前面提到AIDL的流程是:首先创建一个service和AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,客户端在Service的onBind方法中拿到这个类的对象,然后绑定这个service,建立连接后就可以通过这个Stub对象进行RPC。

那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。

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

选用合适的IPC方式

IPC方式的优点缺点和适用场景

更多内容戳这里(整理好的各种文集)

推荐阅读更多精彩内容

  • IPC机制简介 IPC是Inter-Process Communication的缩写,含义就是跨进程通信。首先我们...
    小甜李子阅读 9,946评论 4 18
  • 一、IPC简介 (1)IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨...
    遥遥的远方阅读 3,838评论 0 3
  • 什么是IPC机制? IPC(Inter-Process Communication)是指进程间通信或跨进程通信,也...
    Prisoner_King阅读 39评论 0 0
  • 曾经,宋国最受出名的酒出自蒙城。但所有人都知道,并不是因为酒好。 而是因为那装酒的陶罐,举世罕见。 见过那些陶罐的...
    路人齐小姐阅读 134评论 2 5
  • 芳华
    磨叽猴阅读 27评论 0 0