Android中的IPC介绍

IPC简介和使用场景

IPC简介

IPC(Inter-Process Communication)是指进程间通信,即两个不同进程之间交换数据的过程。

在明确IPC之前,要明白线程和进程的区别:

  • 线程
    线程是进程中的一个实体,是能被操作系统独立调度的基本单位。它不拥有系统资源,但是它可以与同属于同一个进程的其它线程共享进程的全部资源。

  • 进程
    一个执行单元,在PC和移动设备上一般指一个程序或者应用,一个进程可以包含多个线程。每个进程都拥有操作系统分配的相互独立的系统资源,比如CPU、内存等。

在Android系统中,一般一个APP就是一个进程(在无特别代码实现的情况下),UI线程就是这个进程的主线程。如果有耗时操作,会导致主线程堵死,而Android中的主线程负责UI操作,就会出现所谓的ANR(Application Not Responding),影响用户体验。所以,Android中一般需要将耗时操作放在子线程中进行。

IPC的使用场景

在Android开发中,使用IPC的场景一般有以下两种情况:

  • 应用自身原因,需要采用多进程模式现实
    比如,一个APP中的一些模块因为特殊原因需要在单独的进程中运行;或者,需要加大一个APP可使用的内存(Android系统对单个APP可使用的内存大小有限制)。

  • 当前应用需要获取其他应用的数据
    每个APP对应一个进程,所以需要使用IPC。

Android中的多进程模式

这里讨论的多进程模式是指在一个APP中开启多个进程的情况,因为两个APP之间默认就是多进程的。

开启多进程模式

在Android中开启多进程一般只有一种方法,就是给四大组件(Activity,Service、Receiver、Provider)指定android:process属性,因此,不能为一个线程或者实体类指定其运行时所在的进程。

其实还有一种非常规开启多进程的方法,即通过JNI在native层fork一个新的进程,这种方法比较特殊,这里不进行考虑。

在Android中开启多进程的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<activity 
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<intent-filter>
</activity>

<activity
android:name="com.example.ipc.SecondActivity"
android:process=":remote" />

<activity
android:name="com.example.ipc.ThirdActivity"
android:process=".remote" />

在APP中把这三个Activity都打开,通过DDMS可以看到开启了三个进程:

进程信息

这三个进程分别是:

  • com.example.ipc
    MainActivity所在的进程,当前APP的默认进程,和包名相同。

  • com.example.ipc:remote
    SecondActivity所在的进程,当前APP的私有进程。

  • .remote
    ThirdActivity所在的进程,全局进程。

SecondActivity与ThirdActivity所在的进程区别如下:

  • 如果进程名以 : 开始,那么这个进程名会被附加上当前应用的包名,表示该进程是当前应用的私有进程,其它应用不能和它在同一个进程中运行。

  • 如果进程名不以 : 开始,那么进程名不会被附加包名信息,是一个完全的命名,表示该进程是全局进程,其它应用可以通过声明相同的android:sharedUserId属性,和它在同一个进程中运行。

Android系统会为每个应用分配一个唯一的UID(User Id),如果两个应用具有相同的UID,并且签名也相同,那么,这两个应用可以相互访问对方的私有数据,比如组件信息、data目录等;更进一步,如果这两个应用运行在同一个进程时,它们还可以共享内存数据,或者说它们就像是同一个应用的两个部分。

因此,可以把同一个应用中的多进程理解为两个不同的应用使用了相同sharedUserId属性。

开启多进程存在的问题

前面介绍过,操作系统会为每个进程分配相互独立的系统资源,同样,在Android系统中,会为每个进程分配一个独立的虚拟机。由于不同虚拟机在内存分配上的地址空间不同,这就导致不同进程访问同一个类时,会多次实例化这个类,这样,每个进程都会拥有一个这个类的对象,造成多进程间无法共享内存数据。

一般来说,在Android系统中,使用多进程会造成以下几个问题:

  • 静态成员和单例模式失效;
  • 线程同步机制失效;
  • SharedPreferences的可靠性下降;
  • Application会创建多次;

终上所述,在多进程模式中,不同进程的组件会拥有独立的虚拟机、Application和内存空间,这样,多进程之间就无法使用共享内存的方式进行通信,不过,Android系统提供一些跨进程的通信方式。接下来先介绍IPC涉及的一些基本概念,IPC的方式在会下一篇文章中详细分析。

IPC涉及的基本概念

序列化和反序列化

序列化是指将对象转换为可保存的字节序列;反序列化是将字节序列恢复为可使用对象。进程间通信时,一个进程需要将传递的对象序列化,另一个进程接收到序列化对象后需要反序列化,才能正常获取对象的数据。

在Android中,通常有Serializable和Parcelable两种序列化方式,它们之间的有如下区别:

  • Serializable是Java中的序列化接口,其使用起来简单但是开销较大,序列化和反序列化需要大量的I/O操作。
  • Parcelable是Android中的序列化方式,更适用于Android的平台,缺点是使用起来稍微麻烦,但是效率很高。
  • Parcelable主要用于内存序列化上,适合进程间的通信;Serializable更适合文件存储和网络传输。

Android Binder机制

Binder定义及原理

Binder在不同场景下有不同含义:

  • 从机制、模型的角度来说
    Binder是Android系统中特有的一种进程通信方式,即Binder机制模型,用于Android系统实现IPC。

  • 从模型的结构、组成的角度来说
    Binder是一种虚拟的物理设备驱动,即Binder驱动,是ServiceManager连接Manager(比如ActivityManager)和相应ManagerService(比如ActivityManagerService)的桥梁。

  • 从Android应用层来说
    Binder是Client和Server进行通信的媒介,当Client绑定一个Server时,Server就会返回一个包含了服务端业务的Binder对象,Client可以通过这个Binder对象获取Server提供的数据和服务。

  • 从Android代码实现的角度来说
    Binder是一个类,实现了IBinder接口,用于在Android系统中具体实现Binder机制模型。

可以用一张图描述Binder原理:

Binder原理

Binder通信采用的是C/S架构。AMS(ActivityManagerService)可以看成Server;AMP(ActivityManagerProxy)是AMS的代理类,ActivityManager通过AMP与AMS进行通信,可以看成Client。

ServiceManager用于管理系统中的各种服务,不过需要注意的是,这里的ServiceManager是属于Native层(C++)而不是Framework层(Java)。

ioctl是一个系统调用,用于内核空间和用户空间交换数据。通常情况下,进程处于用户空间,并且相互独立,如果要进行通信,需要通过操作系统的内核空间交换数据。

图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制的,那么,图中的每个步骤都有相应的Client端与Server端:

  • 注册服务
    首先AMS注册到ServiceManager。该过程:AMS所在进程(system_server)是客户端,ServiceManager是服务端。

  • 获取服务
    Client进程使用AMS前,须先向ServiceManager中获取AMS的代理类AMP。该过程:AMP所在进程(app process)是客户端,ServiceManager是服务端。

  • 使用服务
    app进程得到的代理类AMP后,就可以直接与AMS所在进程交互。该过程:AMP所在进程(app process)是客户端,AMS所在进程(system_server)是服务端。

图中的Client, Server, ServiceManager之间交互都是虚线表示,是由于它们彼此之间不直接交互,而是通过Binder驱动进行交互,从而实现IPC。其中Binder驱动位于内核空间,Client, Server, ServiceManager位于用户空间。Binder驱动和ServiceManager可以看做是Android平台的基础架构,而Client和Server属于Android的应用层。

如果要进一步了解Binder的底层细节,可以参考彻底理解Android Binder通信架构

Binder的优点

对比Linux(Android基于Linux)上的其他进程通信方式(管道/消息队列/共享内存/信号量/Socket),Binder机制的优点有:

  • 高效
  1. Binder数据复制只需要一次,而管道、消息队列、Socket都需要2次;
  2. 通过驱动在内核空间复制数据,不需要额外的同步处理,而共享内存需要进行同步处理;
  • 安全性高
  1. Binder机制进行通信时,会根据进程的UID/PID进行有效性检测;
  2. 其它进程通信方式对于通信双方的身份并没有做出严格的验证,比如Socket通信的IP地址是客户端手动填入,容易出现伪造;
  • 使用简单
  1. 采用C/S架构;
  2. 实现了“面向对象”的调用方式,在使用Binder时就和调用一个本地对象实例一样;

Binder在Android中的应用

在Android开发中,Binder主要用于Service组件中,Service组件根据是否被绑定分为普通Service(Unbounded Service)和绑定Service(Bounded Service)。其中,普通Service用于完成后台工作任务,不涉及交互,只有绑定Service才会使用Binder进行交互。

在Android中,使用绑定Service有以下三种方式:

  • 扩展Binder类
    如果Service仅供当前应用使用,不需要跨进程工作,则可以实现自己的Binder类,让客户端通过该类直接访问Service中的公共方法。这种方式只在同一个进程中进行交互,不涉及IPC。

  • 使用Messenger
    如果Service需要进行跨进程通信,则需要在Service中使用Messenger。这种方式实现了IPC,并且会在单一线程中创建包含所有请求的队列,以串行的方式处理客户端发来的消息,这样,在进行IPC的时候不需要考虑线程同步的问题。

  • 使用AIDL
    如果Service需要跨进程通信,并且还需要并行处理客户端请求,则需要在Service中使用AIDL。这种方式实现了IPC,还可以让服务端具备多线程处理的能力, 不过,在进行IPC的时候需要考虑线程同步的问题。

其实,Messenger底层是通过AIDL实现的,不过它们最终还是要使用Binder机制实现IPC。

虽然,Messenger和AIDL用于跨进程通信,但是,在同一个进程中,Messenger和AIDL也是能够使用的,不过,这两种情况的底层实现是不一样的,具体表现为:

当Client和Server在同一个进程中时,绑定成功后,Client获取的Binder对象就是Server中定义的Binder对象本身,这样可以直接调用Server中的方法,不需要进行IPC;

当Client和Server在不同的进程中时,绑定成功后,Client获取的Binder对象是Server中Binder的代理对象BinderProxy,具体进行IPC的时候就是通过这个代理对象实现的。

里面的具体细节可以参考Android Binder跨进程与非跨进程的传输异同源码分析

不过,还有一点需要注意的是,Client发起请求时,Client所在的线程会挂起,直到Server所在的进程返回数据,所以,当要调用的远程方法比较耗时,最好不要在UI线程中发起这个远程调用。

坚持原创技术分享,您的支持将鼓励我继续创作!