学习清单:

  • IPC的基础概念
  • 多进程和多线程的概念
  • Android中的序列化机制和Binder
  • Android中的IPC方式
  • Binder连接池的概念及运用
  • 各种IPC的优缺点

一.为什么要学习​​IPC​​?

​IPC​​​是​​Inter-Process Communication​​的缩写,含义是进程间通信,是指两个进程之间进行数据交换的过程。


有些读者可能疑惑: “那什么是进程呢?什么是线程呢?多进程和多线程有什么区别呢?”


  • 进程:是资源分配的最小单位,一般指一个执行单元,在PC和移动设备上指一个程序应用
  • 线程:CPU调度的最小单位,线程是一种有限的系统资源。


两者关系:一个进程可包含多个线程,即一个应用程序上可以同时执行多个任务。

  • 主线程(UI线程):UI操作
  • 有限个子线程:耗时操作

注意:不可在主线程做大量耗时操作,会导致ANR(应用无响应)。解决办法:将耗时任务放在线程中。


​IPC​​​不是​​Android​​​所特有的,​​Android​​​中最有特色的​​IPC​​​方式是​​Binder​​​。而日常开发中涉及到的知识:​​AIDL​​​,插件化,组件化等等,都离不开​​Binder​​​。由此可见,​​IPC​​是挺重要的。

二.核心知识点归纳

2.1​​Android​​中的多进程模式

Q1:开启多线程的方式

  • (常用)在​​AndroidMenifest​​​中给四大组件指定属性​​android:process​


precess的命名规则:

  • 默认进程:没有指定该属性则运行在默认进程,其进程名就是包名
  • 以“:”为命名开头的进程:“:”的含义是在进程名前面加上包名,属于当前应用私有进程
  • 完整命名的进程:属于全局进程,其他应用可以通过ShareUID方式和他跑在用一个进程中(需要ShareUID和签名相同)。


  • (不常用)通过JNI在native层fork一个新的进程。

Q2:多进程模式的运行机制

Andoird为每个进程分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,这也导致了不同虚拟机中访问同一个对象会产生多份副本


带来四个方面的问题:

  • 静态变量和单例模式失效–>原因:不同虚拟机中访问同一个对象会产生多份副本
  • 线程同步机制失效–>原因:内存不同,线程无法同步。
  • SharedPreference的可靠性下降–>原因:底层是通过读写XML文件实现的,发生并发问题。
  • Application多次创建–>原因:Android系统会为新的进程分配独立虚拟机,相当于应用重新启动了一次。


2.2​​IPC​​基础概念


这里主要介绍三方面内容:

  • Serializable
  • Parcelable
  • Binder

只有熟悉这三方面的内容,才能更好理解​​IPC​​的各种方式


2.2.1 什么是序列化

  • 含义:序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
  • 使用场景:需要通过​​Intent​​​和​​Binder​​等传输类对象就必须完成对象的序列化过程。
  • 两种方式:实现​​Serializable​​​/​​Parcelable​​接口。

2.2.2 ​​Serializable​​接口


Java提供的序列化接口,使用方式比较简单:

  • 实体类实现​​Serializable​
  • 手动设置/系统自动生成​​serialVersionUID​


//Serializable Demo
public class Person implements Serializable{
private static final long serialVersionUID = 7382351359868556980L;
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}


这里特别注意一下​​serialVersionUID​​:

  • 含义:是​​Serializable​​接口中用来辅助序列化和反序列化过程。
  • 注意:原则上序列化后的数据中的​​serialVersionUID​​​要和当前类的​​serialVersionUID​相同才能正常的序列化。当类发生非常规性变化(修改了类名/修改了成员变量的类型)的时候,序列化失败。


2.2.3 ​​Parcelable​​接口


是​​Android​​中的序列化接口,使用的时候,类中需要实现下面几点:

  • 实现​​Parcelable​​接口
  • 内容描述
  • 序列化方法
  • 反序列化方法


public class User implements Parcelable {


public int userId;
public String userName;
public boolean isMale;

public Book book;

public User() {
}

public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}

//返回内容描述 return 0 即可
public int describeContents() {
return 0;
}

//序列化
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale ? 1 : 0);
out.writeParcelable(book, 0);
}

//反序列化
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
//从序列化的对象中创建原始对象
public User createFromParcel(Parcel in) {
return new User(in);
}

public User[] newArray(int size) {
return new User[size];
}
};

//从序列化的对象中创建原始对象
private User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readInt() == 1;
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}

@Override
public String toString() {
return String.format("User:{userId:%s, userName:%s, isMale:%s}, with child:{%s}",
userId, userName, isMale, book);
}

}

2.2.4 Serializable和Parcelable接口的比较


​Serializable​​接口

​Parcelable​​接口

平台

Java

Andorid

序列化原理

将一个对象转换成可存储或者可传输的状态

将对象进行分解,且分解后的每一部分都是传递可支持的数据类型

优缺点

优点:使用简单 缺点:开销大(因为需要进行大量的IO操作)

优点:高效 缺点:使用麻烦

使用场景

将对象序列化到存储设备或者通过网络传输

主要用在内存序列化上

2.2.5 Binder

Q1:Binder是什么

  • 从API角度:是一个类,实现​​IBinder​​接口。
  • 从IPC角度:是​​Android​​中的一种跨进程通信方式。
  • 从Framework角度:是​​ServiceManager​​​,连接各种​​Manager​​​和相应​​ManagerService​​的桥梁。
  • 从应用层:是客户端和服务端进行通信的媒介。客户端通过它可获取服务端提供的服务或者数据。

Q2:​​Android​​​是基于​​Linux​​​内核基础上设计的,却没有把管道/消息队列/共享内存/信号量/​​Socket​​​等一些​​IPC​​​通信手段作为Android的主要​​IPC​​​方式,而是新增了​​Binder​​机制,其优点有:

A1:传输效率高、可操作性强


传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。几种数据传输方式比较


方式

拷贝次数

操作难度

Binder

1

简易

消息队列

2

简易

Socket

2

简易

管道

2

简易

共享内存

0

复杂

从Android进程架构角度分析:对于消息队列、Socket和管道来说,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,如图:

                          进阶之路 | 奇妙的 IPC 之旅_安卓


                                                                     消息队列、Socket和管道的数据拷贝

对Binder来说:数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程

A2:实现C/S架构方便


​Linux​​​的众​​IPC​​​方式除了​​Socket​​​以外都不是基于​​C/S​​​架构,而​​Socket​​​主要用于网络间的通信且传输效率较低。​​Binder​​​基于​​C/S​​​ 架构 ,​​Server​​​端与​​Client​​端相对独立,稳定性较好。


A3:安全性高


传统​​Linux​​​ ​​IPC​​​的接收方无法获得对方进程可靠的​​UID/PID​​​,从而无法鉴别对方身份;而​​Binder​​​机制为每个进程分配了​​UID/PID​​​且在​​Binder​​​通信时会根据​​UID/PID​​进行有效性检测。


Q3:​Binder框架定义了哪四个角色呢?

A1:​​Server​​​&​​Client​


服务器&客户端。在​​Binder​​​驱动和​​Service Manager​​提供的基础设施上,进行Client-Server之间的通信。


A2:​​ServiceManager​​:


服务管理者,将​​Binder​​​名字转换为​​Client​​​中对该​​Binder​​​的引用,使得​​Client​​​可以通过​​Binder​​​名字获得​​Server​​​中​​Binder​​实体的引用。


                                   进阶之路 | 奇妙的 IPC 之旅_Binder_02

                                                                      ServiceManager工作机制

A3:​Binder驱动

  • 与硬件设备没有关系,其工作方式与设备驱动程序是一样的,工作于内核态。
  • 提供​​open()​​​、​​mmap()​​​、​​poll()​​​、​​ioctl()​​等标准文件操作。
  • 以字符驱动设备中的​​misc​​​设备注册在设备目录​​/dev​​​下,用户通过​​/dev/binder​​访问该它。
  • 负责进程之间​​binder​​通信的建立,传递,计数管理以及数据的传递交互等底层支持。
  • 驱动和应用程序之间定义了一套接口协议,主要功能由​​ioctl()​​​接口实现,由于​​ioctl()​​​灵活、方便且能够一次调用实现先写后读以满足同步交互,因此不必分别调用​​write()​​​和​​read()​​接口。
  • 其代码位于​​linux​​​目录的​​drivers/misc/binder.c​​中。


ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,该调用传入一个跟设备有关的请求码,系统调用的功能完全取决于请求码


Q4:​​Binder​​ 工作原理是什么

  • 服务器端:在服务端创建好了一个​​Binder​​对象后,内部就会开启一个线程用于接收​​Binder​​驱动发送的消息,收到消息后会执行​​onTranscat()​​,并按照参数执行不同的服务端代码。
  • ​Binder​​驱动:在服务端成功创建​​Binder​​对象后,​​Binder​​驱动也会创建一个​​mRemote​​对象(也是​​Binder​​类),客户端可借助它调用​​transcat()​​即可向服务端发送消息。
  • 客户端:客户端要想访问​​Binder​​的远程服务,就必须获取远程服务的​​Binder​​对象在​​Binder驱动层​​对应的​​mRemote​​引用。当获取到​​mRemote​​对象的引用后,就可以调用相应​​Binder​​对象的暴露给客户端的方法。
    进阶之路 | 奇妙的 IPC 之旅_ipc_03

                                                                                  Binder工作机制


当发出远程请求后客户端会挂起,直到返回数据才会唤醒​​Client​


Q5:当服务端进程异常终止的话,造成Binder死亡的话,怎么办?

在客户端绑定远程服务成功后,给​​Binder​​设置死亡代理,当​​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
}
}
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);

2.3​​Android​​​中的​​IPC​​方式


​Android​​​中的​​IPC​​​方式有很多种,但本质都是基于​​Binder​​构建


                 进阶之路 | 奇妙的 IPC 之旅_ipc_04

                                                                               Android中的IPC方式

2.3.1​​Bundle​

  • 原理:​​Bundle​​​底层实现了​​Parcelable​​接口,它可方便的在不同的进程中传输。
  • 注意:Bundle不支持的数据类型无法在进程中被传递。


  • 小课堂测试:在A进程进行计算后的结果不是​​Bundle​​所支持的数据类型,该如何传给B进程?
  • 答案: 将在A进程进行的计算过程转移到B进程中的一个​​Service​​里去做,这样可成功避免进程间的通信问题。


  • ​Intent​​​和​​Bundle​​的区别与联系:


  • ​Intent​​​底层其实是通过​​Bundle​​进行传递数据的
  • 使用难易:​​Intent​​​比较简单,​​Bundle​​比较复杂
  • ​Intent​​​旨在数据传递,​​bundle​​旨在存取数据


2.3.2 文件共享

  • 概念:两个进程通过读/写同一个文件来交换数据。比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。
  • 适用场景:对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。
  • 特殊情况:​​SharedPreferences​​​也是文件存储的一种,但不建议采用。因为系统对​​SharedPreferences​​的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据。

2.3.3 AIDL

2.3.3.1 概念

​AIDL​​(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用​​AIDL​​​生成可序列化的参数,​​AIDL​​会生成一个服务端对象的代理类,通过它客户端实现间接调用服务端对象的方法。

2.3.3.2 支持的数据类型

  • 基本数据类型
  • ​String​​​和​​CharSequence​


想了解​​String​​​和​​CharSequence​​​区别的读者,可以看下这篇文章:​​String和CharSequence的区别​


  • ​ArrayList​​​、​​HashMap​​​且里面的每个元素都能被​​AIDL​​支持
  • 实现​​Parcelable​​接口的对象
  • 所有​​AIDL​​接口本身


注意:除了基本数据类型,其它类型的参数必须标上方向:​​in、out或inout​​,用于表示在跨进程通信中数据的流向。


2.3.3.3 两种​​AIDL​​文件

  • 用于定义​​Parcelable​​​对象,以供其他​​AIDL​​​文件使用​​AIDL​​中非默认支持的数据类型的。
  • 用于定义方法接口,以供系统使用来完成跨进程通信的。


注意:

  • 自定义的​​Parcelable​​对象必须把​​Java​​​文件和自定义的​​AIDL​​​文件显式的​​import​​进来,无论是否在同一包内。
  • ​AIDL​​​文件用到自定义​​Parcelable​​的对象,必须新建一个和它同名的​​AIDL​​​文件,并在其中声明它为​​Parcelable​​类型。


2.3.3.4 本质,关键类和方法

a:本质是系统提供了一套可快速实现Binder​的工具。

b:关键类和方法是什么?

  • AIDL接口:继承​​IInterface​​。
  • Stub:​​Binder​​的实现类,服务端通过这个类来提供服务。
  • Proxy:服务器的本地代理,客户端通过这个类调用服务器的方法。
  • ​asInterface()​​​:客户端调用,将服务端的返回的​​Binder​​​对象,转换成客户端所需要的​​AIDL​​接口类型对象。


返回对象:

  • 若客户端和服务端位于同一进程,则直接返回​​Stub​​对象本身;
  • 否则,返回的是系统封装后的​​Stub.proxy​​对象。


  • ​asBinder()​​​:返回代理​​Proxy​​​的​​Binder​​对象。
  • ​onTransact()​​​:运行服务端的​​Binder​​线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
  • ​transact()​​​:运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的​​onTransact()​​直到远程请求返回,当前线程才继续执行。

进阶之路 | 奇妙的 IPC 之旅_ipc_05

                                                                                AIDL工作机制

2.3.3.5 实现方法


如果感兴趣的读者想要了解具体的​​AIDL​​​实现​​IPC​​​的流程,笔者分享一篇文章:​​android跨进程通信(IPC):使用AIDL​


A.服务端:

  • 创建一个​aidl文件
  • 创建一个​​Service​​​,实现​​AIDL​​​的接口函数并暴露​​AIDL​​接口。

B.客户端:

  • 通过​​bindService​​​绑定服务端的​​Service​​;
  • 绑定成功后,将服务端返回的​​Binder​​对象转化成​​AIDL​​​接口所属的类型,进而调用相应的​​AIDL​​中的方法。


总结:服务端里的某个​​Service​​​给和它绑定的特定客户端进程提供​​Binder​​​对象,客户端通过​​AIDL​​​接口的静态方法​​asInterface()​​​ 将​​Binder​​​对象转化成​​AIDL​​接口的代理对象,通过这个代理对象就可以发起远程调用请求。


2.3.3.6 可能产生​​ANR​​的情形

A.客户端:

  • 调用服务端的方法是运行在服务端的​​Binder​​线程池中,若主线程所调用的方法里执行了较耗时的任务,同时会导致客户端线程长时间阻塞,易导致客户端​​ANR​​。
  • 在​​onServiceConnected()​​​和​​onServiceDisconnected()​​​里直接调用服务端的耗时方法,易导致客户端​​ANR​​。

B.服务端:

  • 服务端的方法本身就运行在服务端的​Binder线程中,可在其中执行耗时操作,而无需再开启子线程
  • 回调客户端Listener的方法是运行在客户端的​​Binder​​线程中,若所调用的方法里执行了较耗时的任务,易导致服务端​​ANR​​。


解决客户端频繁调用服务器方法导致性能极大损耗的办法:实现观察者模式

即当客户端关注的数据发生变化时,再让服务端通知客户端去做相应的业务处理。


2.3.3.7 解注册失败的问题

  • 原因:​​Binder​​​进行对象传输实际是通过序列化和反序列化进行,即​​Binder​​会把客户端传递过来的对象重新转化并生成一个新的对象,虽然在注册和解注册的过程中使用的是同一个客户端传递的对象,但经过​​Binder​​传到服务端后会生成两个不同的对象。另外,多次跨进程传输的同一个客户端对象会在服务端生成不同的对象,但它们在底层的​​Binder​​对象是相同的。
  • 解决办法:当客户端解注册的时候,遍历服务端所有的​​Listener​​​,找到和解注册​​Listener​​​具有相同的​​Binder​​​对象的服务端​​Listener​​,删掉即可。


需要用到​​RemoteCallBackList​​​:​​Android​​​系统专门提供的用于删除跨进程​​listener​​的接口。其内部自动实现了线程同步的功能。


2.3.4​​Messager​

Q1.什么是Messager

A1:​​Messager​​​是轻量级的​​IPC​​​方案,通过它可在不同进程中传递​​Message​​对象。

Messenger.send(Message);

Q2:特点是什么

  • 底层实现是​​AIDL​​​,即对​​AIDL​​进行了封装,更便于进行进程间通信。
  • 其服务端以串行的方式来处理客户端的请求,不存在并发执行的情形,故无需考虑线程同步的问题。
  • 可在不同进程中传递​​Message​​​对象,​​Messager​​​可支持的数据类型即​​Messenge​​可支持的数据类型。


​Messenge​​可支持的数据类型:

  • ​arg1​​​、​​arg2​​​、​​what​​​字段:​​int​​型数据
  • ​obj​​​字段:​​Object​​​对象,支持系统提供的​​Parcelable​​对象
  • ​setData​​​:​​Bundle​​对象


  • 有两个构造函数,分别接收​​Handler​​​对象和​​Binder​​对象。

Q3:实现的方法


读者如果对​​Messenger​​​的具体使用感兴趣的话,可以看下这篇文章:​​IPC-Messenger使用实例​


A1:服务端:

  • 创建一个​​Service​​用于提供服务;
  • 其中创建一个​​Handler​​用于接收客户端进程发来的数据
  • 利用​​Handler​​​创建一个​​Messenger​​对象;
  • 在​​Service​​​的​​onBind()​​​中返回​​Messenger​​​对应的​​Binder​​对象。

A2:客户端:

  • 通过​​bindService​​绑定服务端的​​Service​​;
  • 通过绑定后返回的​​IBinder​​对象创建一个​​Messenger​​,进而可向服务器端进程发送​​Message​​数据。(至此只完成单向通信)
  • 在客户端创建一个​​Handler​​并由此创建一个​​Messenger​​,并通过​​Message​​的​replyTo字段传递给服务器端进程。服务端通过读取​​Message​​得到​​Messenger​​对象,进而向客户端进程传递数据。(完成双向通信)
    进阶之路 | 奇妙的 IPC 之旅_安卓_06

                                                                                             Messenger通信


Q4:缺点:

  • 主要作用是传递​​Message​​,难以实现远程方法调用。
  • 以串行的方式处理客户端发来的消息的,不适合高并发的场景。


解决方式:使用​​AIDL​​的方式处理IPC以应对高并发的场景


2.3.5​​ContentProvider​


​ContentProvider​​​是​​Android​​提供的专门用来进行不同应用间数据共享的方式,底层同样是通过​​Binder​​实现的。


  • 除了​​onCreate()​​​运行在UI线程中,其他的​​query()​​​、​​update()​​​、​​insert()​​​、​​delete()​​​和​​getType()​​​都运行在​​Binder​​线程池中。
  • ​CRUD​​四大操作存在多线程并发访问,要注意在方法内部要做好线程同步。
  • 一个​​SQLiteDatabase​​​内部对数据库的操作有同步处理,但多个​​SQLiteDatabase​​之间无法同步。

2.3.6​​Socket​


​Socket​​不仅可以跨进程,还可以跨设备通信


Q1:使用类型是什么?

  • 流套接字:基于​​TCP​​协议,采用流的方式提供可靠的字节流服务。
  • 数据流套接字:基于​​UDP​​协议,采用数据报文提供数据打包发送的服务。

Q2:实现方法是什么?

A1:服务端:

  • 创建一个​​Service​​​,在线程中建立​​TCP​​服务、监听相应的端口等待客户端连接请求;
  • 与客户端连接时,会生成新的​​Socket​​对象,利用它可与客户端进行数据传输;
  • 与客户端断开连接时,关闭相应的​​Socket​​并结束线程。

A2:客户端:

  • 开启一个线程、通过​​Socket​​发出连接请求;
  • 连接成功后,读取服务端消息;
  • 断开连接,关闭​​Socket​​。

2.3.7 优缺点比较

名称

优点

缺点

适用场景

​Bundle​

简单易用

只能传输​​Bundle​​支持的数据类型

四大组件间的进程间通信

文件共享

简单易用

不适合高并发场景,无法做到进程间的即时通信

无并发访问,交换简单数据且实时性不高

​AIDL​

支持一对多并发和实时通信

使用稍复杂,需要处理线程同步

一对多且有​​RPC​​需求

​Messenger​

支持一对多串行通信

不能很好处理高并发,不支持​​RPC​​​,只能传输​​Bundle​​支持的数据类型

低并发的一对多

​ContentProvider​

支持一对多并发数据共享

可理解为受约束的​​AIDL​

一对多进程间数据共享

​Socket​

支持一对多并发数据共享

实现细节繁琐

网络数据交换

2.4​​Binder​​连接池


有多个业务模块都需要​​AIDL​​​来进行​​IPC​​​,此时需要为每个模块创建特定的​​aidl​​​文件,那么相应的​​Service​​​就会很多。必然会出现系统资源耗费严重、应用过度重量级的问题。因此需要​​Binder​​​连接池,通过将每个业务模块的​​Binder​​请求统一转发到一个远程​​Service​​​中去执行的方式,从而避免重复创建​​Service​​。


Q1:工作原理是什么

每个业务模块创建自己的​​AIDL​​接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个​​Service​​​,服务器提供一个​​queryBinder​​​接口,它会根据业务模块的特征来返回相应的​​Binder​​​对像,不同的业务模块拿到所需的​​Binder​​对象后就可进行远程方法的调用了。

                  进阶之路 | 奇妙的 IPC 之旅_Binder_07

Binder连接池工作原理

Q2:实现方式是什么

  • 为每个业务模块创建​​AIDL​​接口并具体实现
  • 为​​Binder​​​连接池创建​​AIDL​​​接口​​IBinderPool.aidl​​并具体实现
  • 远程服务​​BinderPoolService​​​的实现,在​​onBind()​​​返回实例化的​​IBinderPool​​实现类对象
  • ​Binder​​连接池的具体实现,来绑定远程服务
  • 客户端的调用

三.碎碎念


恭喜你,已经完成了这次奇妙的​​IPC​​之旅了,如果你感到对概念还是有点模糊不清的话,没关系,很正常,不用太纠结于细节,你可以继续进行下面的旅程了,未来的你,再看这篇文章,也许会有更深的体会,到时候就会有茅舍顿开的感觉了。未来的你,一定会更优秀!!!

路漫漫其修远兮,吾将上下而求索。《离骚》–屈原