进阶之路 | 奇妙的IPC之旅

Posted xcynice

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶之路 | 奇妙的IPC之旅相关的知识,希望对你有一定的参考价值。

前言

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

我的GIthub博客

学习清单:

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

一.为什么要学习IPC

IPCInter-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 什么是序列化

  • 含义:序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
  • 使用场景:需要通过IntentBinder等传输类对象就必须完成对象的序列化过程。
  • 两种方式:实现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;
    }

    //返回内容描述
    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进程?

Answer: 将在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实现的。

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

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


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

本文参考链接:

以上是关于进阶之路 | 奇妙的IPC之旅的主要内容,如果未能解决你的问题,请参考以下文章

进阶之路 | 奇妙的Window之旅

进阶之路 | 奇妙的Activity之旅

进阶之路 | 奇妙的Thread之旅

进阶之路 | 奇妙的Drawable之旅

进阶之路 | 奇妙的View之旅

我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印出C++的函数耗时以及代码片段耗时详情