Android开发艺术探索 | IPC机制

第二章 IPC机制

学习清单:

  • Android中的多进程模式

  • IPC基础概念

    • 序列化

      • Serializable接口

      • Parcelable接口

    • Binder

  • Android中的IPC方式

    • Bundle

    • 文件共享

    • Messenger

    • AIDL

    • ContentProvider

    • Socket

  • Binder连接池

  • 如何选用合适的IPC方式


一.Android中的多进程模式

在谈IPC之前, 我们首先需要理解Android中的多进程模式.

a. 什么是线程? 什么是进程?

  • 线程是CPU调度的最小单元

  • 一个进程可以有多个线程

  • 一个进程至少要有一个线程, 即主线程, 在Android中也叫做UI线程

注意: 如果一个进程中需要执行一个耗时的操作, 将其放在主线程执行, 则会造成应用无响应, 也就是ANR(Application Not Responding), 如果要解决这个问题, 就需要用多线程, 将耗时操作交给别的线程处理

b. 多进程有什么用?

  • 某些模块因特殊原因要运行在单独进程中

  • 为加大一个应用可使用的内存,需通过多进程来获取多份内存空间

1. 开启多进程模式:

  • 在Android中使用多进程只有一种方法, 那就是给四大组件(Activity | Service | Receiver | ContentProvider) 在AndroidMenifest中指定android:process属性, 没有指定process属性时, 默认进程的进程名是包名.

补充: 在为android:process属性命名时, 有两种方式:

  1. android:process=":remote": 以 " : " 开头是在当前进程名前附加上包名的简写方式, 以这种方式开头的进程属于私有进程, 其他应用不可以和它跑在同一个进程中
  1. android:process="com.example.main.remote": 完整的命名方式, 该方式属于全局进程, 其他应用通过ShareUID方式可以和它跑在同一个线程

2. 开启多进程模式所带来的问题:

静态成员和单例模式完全失效

  • Android系统为每一个进程都分配了一个独立的虚拟机, 导致在不同的虚拟机中访问同一个类的对象会产生多个副本

线程同步机制完全失效

  • 原因同上

SharedPreferences的可靠性下降

  • SharedPreferences不支持两个进程同时进行读写操作,即不支持并发读写,有一定几率导致数据丢失

Application会多次创建

  • Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次

二.IPC基础概念

a. IPC是什么?

  • IPC(Inter-Process Communication,跨进程通信):指两个进程之间进行数据交换的过程

  • 任何一个操作系统都有对应的IPC机制

b. IPC的使用场景:

  • 当某个进程需要向别的进程获取数据

c. Android的进程架构:

  • 每一个Android进程都是独立的,且都由两部分组成,一部分是用户空间,另一部分是内核空间,如下图:
image

1. 序列化:

a. 序列化的介绍:

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

b.Serializable接口:

实现方式:

  • 实现Serializable接口

  • 为该类指定SerialVersionUID (可选)

注意: 不指定UID可以实现序列化, 但是会影响反序列化. 所以我们应该手动去设定UID的值: private static final long serialVersionUID = 1L;


c. Parcelable接口:

实现方式:

  • 实现Parcelable接口

  • 实现接口中的各种方法, 各方法功能如下:
    image

b.Serializable接口和Parcelable接口的比较:

image

2. Binder:

a.概念:

  • 从API的角度: 是一个类, 实现Binder接口

  • 从IPC的角度: 是Android中的一种跨进程通信方式

  • 从Framework角度: 是ServiceManager连结各种Manager和相应ManagerService的桥梁

  • 从应用层的角度: 是客户端服务端进行通信的媒介, 客户端通过连接他来获取服务端提供的服务或者数据

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

  • 传输效率高、可操作性强:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。几种数据传输方式比较:
方式 拷贝次数 操作难度
Binder 1 简易
消息队列 2 简易
Socket 2 简易
管道 2 简易
共享内存 0 复杂
  • 数据从发送方的缓存区拷贝到了内核和缓存区, 而接收方的缓存区与内核的缓存区映射的是同一个物理地址, 节省了一次数据拷贝时间, 如图:
image-20200601115119909.png
  • 实现C/S架构方便:Linux的众IPC方式除了Socket以外都不是基于C/S架构,而Socket主要用于网络间的通信且传输效率较低。Binder基于C/S 架构 ,Server端与Client端相对独立,稳定性较好。

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

c.Binder框架定义了四个角色: Server, Client, ServiceManager和Binder驱动

其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间, 如图:

image-20200601115427397.png

下面简单介绍这四个角色:

  • ServiceManager: 服务的管理者, 将Binder名字转换为Client中对改Binder的引用, 使得Client可以通过Binder名字获得Service中Binder实体的引用, 如图:
image-20200601115645032.png
  • Binder驱动:

    • 与硬件设备没有直接关系, 其工作方式与设备驱动程序是一样的, 工作于内核态

    • 提供open(), mmap(), poll(), ioctl()等标准文件操作

    • 以字符驱动设备中的misc设备注册在设备目录/dev下, 用户通过/dev/binder访问它

    • 负责进程间binder通信的建立, 传递, 计数管理及数据的传递交互等底层支持

    • 驱动和应用程序之间定义了一套接口协议, 主要功能有ioctl()接口实现, 由于ioctl()灵活方便且能一次调用实现先写后读以满足同步交互, 因此不必分别调用write()和read()接口

    • 其代码位于linux目录的driver/misc/binder.c中

  • Service&Client:

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

d.代理模式Proxy: 给某个对象一个代理对象, 并由代理对象控制对原对象的访问, 如图:

image-20200601121454234.png

代理模式的组成:

  • Abstract Subject (抽象主题) : 声明Real Subject和Proxy的共同接口,这样在任何可以使用Real Subject的地方都可以使用Proxy

  • Real Subject (真实主题) : 定义了proxy所代表的Real subject

  • Proxy Subject (代理主题) :

    • 内部含有一个Real Subject的引用, 可在任何时候操作目标对象

    • 提供一个代替Real Subject相同的接口, 可在任何时候替代目标对象

e. Binder 工作模式:

  • 服务器端:在服务端创建好了一个Binder对象后,内部就会开启一个线程用于接收Binder驱动发送的消息,收到消息后会执行onTransact(),并按照参数执行不同的服务端代码

  • Binder驱动:在服务端成功Binder对象后,Binder驱动也会创建一个mRemote对象(也是Binder类),客户端可借助它调用transact()即可向服务端发送消息

  • 客户端:客户端要想访问Binder的远程服务,就必须获取远程服务的Binder对象在Binder驱动层对应的mRemote引用。当获取到mRemote对象的引用后,就可以调用相应Binder对象的暴露给客户端的方法


3. IPC方式

image-20200601152330202.png

由上图可以发现, 这些IPC方式全都是通过Binder实现的, 只是封装的方法不同, 接下来我们分别介绍这六种IPC方式

1.使用Bundle

a. Bundle: 支持在Activity, Service和Receiver之间通过Intent.putExtra()传递bundle数据

Intent intent = new Intent();
 Bundle bundle = new Bundle();
 bundle.putString("xxx","xxx");
 intent.putExtra("data", bundle);

b. 原理: Bundle实现了Parcelable接口, 它可以方便的在不同的进程中传输

c. 注意: Bundle不支持的数据类型无法在进程间传输


2.使用文件共享

a. 文件共享: 两个进程通过读/写同一个文件来交换数据。比如A进程把数据写入文件,B进程通过读取这个文件来获取数据

b. 适用情况:对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题

c. 虽然SharedPreferences也是文件存储的一种,但不建议采用

  • 原因: 系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据

3.使用Messenger

a. Messenger: 信使, 轻量级IPC方式, 通过它在不同进程间传递Message对象, 而在Message中放我们要传递的数据, 从而实现IPC

相关记忆:

  • Handler: 主要用于线程之间的数据通信
  • Messenger: 进程间的数据通信

b. 特点:

  • 底层实现的是AIDL, 即对AIDL进行了封装, 更便于进行进程间通信

  • 其服务端以串行的方式来处理客户端的请求, 不存在并发执行的情形, 故无需考虑线程同步的问题

  • 可在不同进程中传递Message对象, Messenger可支持的数据类型即Message可支持的数据类型

  • arg1, arg2, what字段: int
  • obj字段: Object对象, 支持系统提供的Parcelable对象
  • setData: Bundle对象
  • 有两个构造函数, 分别接收Handler对象和Binder对象

c. 实现方法

  • 服务端:

    • 创建一个Service来处理客户端连接请求

    • 创建一个Handler来获取Messenger对象

    • 在Service的onBind()中返回这个Messenger对象底层的Binder

  • 客户端:

    • 绑定服务端的Service

    • 通过绑定成功后返回的IBinder对象创建一个Messenger对象

如果需要客户端能够回应服务端:

  • 创建一个Handler和一个新的Messenger
  • 将Messenger这个对象通过MessagereplyTo参数传递给服务端
  • 服务端通过replyTo参数就能够回应客户端

d. Messenger的缺点:

  • 主要传递的是Message, 难以实现远程方法调用

  • 以串行的方式处理客户端发来的消息, 不适合高并发场景

解决方法: 使用AIDL来实现IPC


4.使用AIDL

a.AIDL(Android Interface Definition Language,Android接口定义语言): 可以利用它定义客户端与服务均认可的编程接口, 以便二者使用进程间通信 (IPC) 进行相互通信, AIDL会生成一个服务端的代理类, 通过它客户端实现间接调用服务端的方法

b. 支持的数据类型:

  • 基本数据类型(int, long, char, boolean, double等)

  • StringCharSequence

  • List: 只支持ArrayList, 里面每个元素都必须被AIDL支持

  • Map: 只支持HashMap, 里面每个元素都必须被AIDL支持, 包括key和value

  • Paecelable: 所有实现了Parcelable接口的对象

  • AIDl: 所有的AIDL接口本身也可以在AIDL文件中使用

注意: 除了基本数据类型, 其他类型的参数必须标上方向: in, out, inout, 用于表示数据的流向

  • in
*   表示数据只能由客户端流向服务端
    
    
*   服务端将会接收到这个对象的完整数据, 但在服务端修改它不会对客户端输入的对象产生影响
  • out
*   表示数据只能由服务端流向客户端
    
    
*   服务端将会接收到这个对象的的空对象, 但在服务端对接收到的空对象有任何修改之后客户端将会同步变动
  • inout
*   表示数据可在服务端与客户端之间双向流通
    
    
*   服务端将会接收到客户端传来对象的完整信息, 且客户端将会同步服务端对该对象的任何变动

c. 两种AIDL文件

  • 用于定义parcelable对象, 以供其他AIDL文件使用AIDL中非默认支持的数据类型的

  • 用于定义方法接口, 以供系统使用来完成跨进程通信的

注意:

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

d. AIDL本质上是系统提供了一套可快速实现Binder的工具. 关键类和方法:

  • AIDL接口: 继承IInterface

  • Stub类: Binder的实体类, 服务端通过这个类来提供服务

  • Proxy类: 服务器的本地代理, 客户端通过这个类来获取服务

  • asInterface(): 客户端调用, 将服务端返回的Binder对象, 转换成客户端所需的AIDL接口类型对象. 返回对象:

    • 若客户端和服务端位于同一进程, 则直接返回Stub对象本身

    • 否则, 返回系统封装后的Stub.proxy对象

  • asBinder(): 根据当前调用情况返回代理Proxy的Binder对象

  • onTransact(): 运行服务端的Binder线程池中, 当客户端发起跨进程请求时, 远程请求会通过系统底层封装后交由此方法处理

  • transact(): 运行在客户端, 当客户端发送远程请求的同时将当前线程挂起. 之后调用服务端的onTransact()直到远程请求返回, 当前线程才继续执行

image-20200602153715489.png

e. 实现方法

  • 服务端:

    • 创建一个AIDL文件

    • 创建一个Service, 实现AIDL的接口函数并暴露AIDL接口

  • 客户端:

    • 通过bindService绑定服务端的Service

    • 绑定成功后, 将服务端返回的Binder对象转化为AIDL接口所属的类型, 进而调用AIDL中提供的方法

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

f. 可能产生ANR的情形

  • 对于客户端

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

    • onServiceConnected()onServiceDisconnected()里直接调用服务端的耗时方法, 易导致客户端ANR

  • 对于服务端

    • 服务端的方法本身就运行在服务端的Binder线程中, 可在其中执行耗时操作, 而无需再开启子线程

    • 回调客户端Listener的方法是运行在客户端的Binder线程中, 若所调用的方法里执行了较耗时的任务, 易导致服务端ANR

提示: 解决客户端频繁调用服务器方法导致性能极大损耗的办法 : 实现观察者模式. 即当客户端关注的数据发生变化时, 再让服务端通知客户端去做相应的业务处理

g. AIDL解注册失败

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

  • 解决办法:当客户端解注册的时候,遍历服务端所有的Listener,找到和解注册Listener具有相同的Binder对象的服务端Listener,删掉即可

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


5.使用ContentProvider

a. ContentProvider: Android中提供的专门用于不同应用间进行数据共享的方式,底层实现使用Binder

b. 特点:

  • 相比于AIDL,使用更加简单,无需关心底层细节

  • 系统预置了许多ContentProvider如:通讯录信息、日程表信息等

  • ContentProvider主要以表格的形式来组织数据,使用方法与数据库很类似

  • ContentProvider还支持文件数据,如图片、视频等

c. 使用方法:

  • 服务端:

    • 新建一个类继承自ContentProvider, 并实现六个抽象方法:

      • onCreate(): ContentProvider的创建, 一般用于一些初始化动作

      • query(): 对数据表进行查询操作

      • insert(): 对数据表进行插入操作

      • delete(): 对数据表进行删除操作

      • update(): 对数据表进行更新操作

      • getType(): 返回一个Uri请求所对应的MIME(媒体)类型, 如图片、视频

      注意:

      • update, insert和delete均运行在Binder线程池中, 而onCreate运行在主线程(UI线程), 即不能在onCreate中执行耗时操作
      • 一个SQLiteDatabase内部对数据库的操作有同步处理, 但多个SQLiteDatabase之间无法同步
    • 注册ContentProvider


  • 客户端: 通过getContentResolver()获取到ContentResolver对象, 并通过此对象执行相应的操作

6.使用Socket

a. Socket: 套接字, 是网络通信中的概念, 分为: 流式套接字和用户数据报套接字, 分别对应TCP和UDP

网络传输协议:

TCP: 面向连接的协议, 提供稳定的双向通信功能, 具有很高的稳定性

UDP: 无连接, 提供不稳定的单向通道, 也可实现双向功能, 效率更高, 但不能保证数据安全送达

b. 注意:

  • 不能在主线程(UI线程)中访问网络

  • 使用Socket进行通信, 需要声明权限


 

c. 使用方法:

  • 服务端:

    • 创建一个Service, 在线程中建立端口, 等待客户端连接请求

    • 在与客户端连接后, 会生成一个新的Socket, 通过它可以和客户端进行数据传输

    • 在于客户端断开连接后, 关闭相应的Socket并结束线程

  • 客户端:

    • 开启一个线程, 通过Socket发出连接请求

    • 连接成功后, 通过这个Socket读取服务端消息

    • 断开连接, 关闭Socket

image-20200604165308839.png

以上六种IPC方式的优缺点及使用场景如下图:

image-20200604165606377.png

4.Binder连接池

  • 背景: 当多个业务模块都需要使用到AIDL来进行IPC时, 则需要为每一个模块创建对应的aidl文件, 与之对应的则要创建许多的Service服务. 这样会极大的占用我们有限的系统资源

  • 功能: 将每一个模块的Binder请求都统一转发到一个远程Service中去执行, 避免重复创建新的Service

    • 原理: 每个业务模块有着自己的AIDL接口并实现, 然后向服务端提供自己的唯一标识和Binder对象. 服务端只需要一个Service并提供一个queryBinder接口, 它将根据业务模块的标识来返回相应的Binder对象, 不同的业务模块拿到自己的所需的Binder对象就可以进行对应的远程操作
  • 实现:

    • AIDL接口

      • 实现相关模块的AIDL接口

      • 创建一个IBinderPool.aidl文件, 获取相关模块所需的AIDL接口

        interface IBinderPool {
          IBinder queryBinder(int binderCode);
         }
        
      • 创建一个BinderPool文件, 实现IBinderPool接口

       public static class BinderPoolImpl extends IBinderPool.Stub {
          public BinderPoolImpl() {
          super();
          }
         ​
          @Override
          public IBinder queryBinder(int binderCode) throws RemoteException {
          IBinder binder = null;
          switch (binderCode) {
          case BINDER_SECURITY_CENTER: {
          binder = new SecurityCenterImpl();
          break;
          }
          case BINDER_COMPUTE: {
          binder = new ComputerImpl();
          break;
          }
          default:
          break;
          }
          return binder;
          }
          }
      
      • Binder连接池的具体实现, 来绑定远程服务
    • 服务端: 远程服务BinderPoolService实现, 在onBind()处返回实例化的IBinderPool实体类对象

    • 客户端:

      • 通过BinderPool类中的getInstance(Context)获取BinderPoll类实例

      • 通过BinderPool类实例里实现的queryBinder(int)方法获取所需要的Binder对象


总结

  • IPC是指两个进程之间进行数据交换的过程

  • Android中的IPC方式底层都是由Binder实现的, 由此可见Binder在Android中的重要性

  • 要根据不同的需求, 使用不同的IPC方式, 因地制宜

  • 要多用, 不然会忘

你可能感兴趣的:(Android开发艺术探索 | IPC机制)