您的位置:首页 > 其它

进阶之路 | 奇妙的IPC之旅

2020-02-28 18:32 21 查看

前言

本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:

我的GIthub博客

学习清单:

  • 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和管道来说,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,如图:

对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
实体的引用。

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
    对象的暴露给客户端的方法。

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

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
构建的

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()
    直到远程请求返回,当前线程才继续执行。

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
    对象,进而向客户端进程传递数据。(完成双向通信)

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
对象后就可进行远程方法的调用了。

Q2:实现方式是什么

读者如果对具体的实现方式感兴趣的话,可以看一下这篇文章:Android IPC机制(四):细说Binder连接池

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

三.碎碎念

恭喜你,已经完成了这次奇妙的

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

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

如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: