深入浅出Android系列之Android进程间通信

前言:

在深入开发Android之后,感觉目前的Android开发已经满足不了我了,所以有很多新的方面的尝试,比如横向iOS方面的开发,但是也想着为自己找到Android进阶的路径,所以就想开始深入Android的学习,本系列(深入浅出Android系列)作为我Android进阶过程中学习的记录,也是我在一段时间内的理解与解读,本文也是本系列的第一篇文章。


一、 Linux进程间通信

在深入Android进程通信前,首先我们要了解一下,Linux目前拥有的IPC机制

1) PIPE(匿名管道,也称为管道)

匿名管道通过pipe函数创建,pipe函数通过在内核的虚拟文件系统中创建两个pipefs虚拟文件,并返回这两个虚拟文件描述符,通过这两个虚拟文件描述符进行通信。管道就是一个被内核所管理的缓冲,就像我们把一张纸放在内存里一样。这条管线的一端与处理的输出相连。该过程将信息放入管道。管道的另外一端与一个处理程序相连,该处理程序获取该处理程序所需要的信息。

其中需要注意的

  • 匿名管道是单向半双工的,单向意味着数据只能向一个方向流动。需要双方通信时,需要建立起两个管道。半双工意味着不能同时进行读写。

  • 只能用于父子进程或兄弟进程之间(具有亲缘关系的进程)。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。

  • 管道只能在本地计算机中使用,而不可用于网络间的通信。


2) FIFO(命名管道)

  • 命名管道也是半双工,但是是双向半双工,可以同时进行读写操作

  • 允许没有亲缘关系的进程进行通信


3) Share Memory(共享内存)

4) Memory Map(内存映射)

5) Socket(套接字)

  •  套接字机制不但可以在单机的不同进程间通信,而且可以在跨网机器间进程通信。

  •  套接字的创建和使用与管道是有区别的,套接字明确的将客户端与服务器区分开来,可以实现多个客户端连到同一个服务器

  •  传输数据为字节级,传输数据可自定义,数据量小(对于手机应用讲:费用低)

  •  可以加密,数据安全性强传输数据时间短,性能高


二、 Android进程间通信的分类

Android中常用的进程间通信有以下几个分类,他们各自有各自的特点,有延续Linux的,也有Android特色的

Binder

Android中最常用的跨进程通信方案,他有很多特点

  • 从性能方面说,Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从这个角度看,Binder性能仅次于共享内存

  • 从安全的角度来说,传统 Linux IPC的接收方不能获取对方进程可靠的 UID/PID,因此也不能对对方的身份进行认证,而传统 Linux IPC没有任何的保护措施,完全是通过上层协议来保证的。Android系统对每一个已安装的应用都有一个独立的 UID,因此一个进程的 UID就成为一个很好的标识。对于传统的 IPC协议,只有用户才能向报文中添加 UID/PID协议;此外, IPC机制自身也仅能将可信标识添加到内核中。其次,传统的 IPC接入点都是开放式的,不能设置私人信道。从安全性上来说,绑定器更加安全

  • 从稳定性的角度,Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;所以从这稳定性角度看,Binder架构优越于共享内存。

Socket

Android中较常用的跨进程通信方案


Handler

Android中常用的进程间(线程间)通信方案


三、 传统IPC通信

在上面一点中讲到了Binder的特点中,从性能与安全的角度分析了,那么我们来简单看一下,传统的IPC是怎么通信的

在传统的IPC通信中

第一步:消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态

第二步:内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中

第三步:接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区

所以,在传统的IPC通信方案中至少需要拷贝两次数据,在系统层次来说,频繁的交互,多一次拷贝就会多消耗非常多性能


那么,Binder是如何通信的,然后避免这个多余的消耗的呢?


四、Binder

在 Binder 模型中一共有 4 个主要角色,它们分别是:Client、Server、Binder 驱动和 ServiceManager. Binder 的整体结构是基于 C|S 结构的,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。Service Manager和Binder驱动由系统提供,而Client、Server由应用程序来实现。其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。

很多文章都有提到一个点,就是Binder各个元素的组成和TCP/IP有相同之处,就是因为Binder跟TCP/IP一样采用了分层架构,简单做个类比,可以这样看


  • Binder驱动 -> 路由器
  • Service Manager -> DNS
  • Binder Client -> 客户端
  • Binder Server -> 服务端



然后,要简单讲一个概念——内存映射,这是Linux下的概念,在 Binder IPC机制中所包含的内存映射是由操作系统中的一种内存映射方式—— mmap()来实现的。所谓的内存映射,就是把一片存储在用户空间中的内存,映射到内核空间中去。在此之后,用户对这块内存区域的修改,将会直接反馈至核心记忆体中;相反,在这个范围内,内核空间发生的变化也会影响到用户空间。

内存映射可以有效地降低存储过程中的复制量,提高系统与系统之间的交互效率。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。由于这个原因,内存映射能够提供对进程间通信的支持。


五、 Binder进程间通信

在上一点的描述后,应该对于Binder有了一定的了解,接下来,就是讲述一下Binder是如何通信的

第一步:Binder 驱动在内核空间创建一个数据接收缓存区

第二步:在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系

第三步:发送方过程使用 copyfromuse()将数据复制到内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信


六、 Android进程间通信的应用

1)Activity应用

Activity是最简单也是最常用的应用,其中有一个方法,可以用于IPC也可以用于Android进程间的通信——startActivity

//显式Intent,用于进程间的通信
startActivity(Intent(MainActivity@this, SecondActivity::class.java))
//隐式Intent,用于跨进程通信
 val intent = Intent(Intent.ACTION_DIAL)
 val url = Uri.parse("tel:10086")
 intent.data = url
 startActivity(intent)


2)Content provider应用

当我们的应用需要向其他应用提供数据,或者接受数据时,就需要通过Content provider,他也是一种IPC的应用,例如获取媒体信息、联系人方式等,参考代码如下

val projection = arrayOf(media-database-columns-to-retrieve)
val selection = sql-where-clause-with-placeholder-variables
val selectionArgs = values-of-placeholder-variables
val sortOrder = sql-order-by-clause
applicationContext.contentResolver.query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    while (cursor.moveToNext()) {
        // Use an ID column from the projection to get
        // a URI representing the media item itself.
    }
}


3)Broadcast应用

广播可以接受到很多方面的数据,例如设备开始充电、设备关机、设备启动等等,我们在应用中也可以使用广播来进行通信,但是需要主要的是广播进行通信会有很多坑,比如说接收不到、延迟等,参考代码如下

val intent=Intent("com.example.MyApplication.MY_BROADCAST")
intent.setPackage(packageName)
sendBroadcast(intent)


4)Service应用

作为一个在后台可以长期运行的组件,他可以跟应用内进行通信,也可以跨应用进行通信,参考代码如下

 startService(Intent(baseContext, MyService::class.java))


七、 Binder的延伸

Binder其实就是四个系统组件,就是第四点说的那样,Client、Server、Service Manager和Binder驱动程序

1)Client

Server向ServiceManager注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向ServiceManager请求访问某个Binder。

2)Server

一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。

3)Service Manager

ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,但并没有采用libbinder中的多线程模型来与Binder驱动通信,而是自行编写了binder.c直接和Binder驱动来通信,并且只有一个循环binder_loop来进行读取和处理事务,这样的好处是简单而高效。ServiceManager是由init进程通过解析init.rc文件而创建的。

4)Binder驱动程序

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。


八、 L inux进程间通信在Android中的系统应用

1)管道在Android中的应用

在Android 6.0 以下版本中,主线程Looper的唤醒就使用到了管道。


2)消息队列在Android中的应用

受限于性能,数据量等问题的限制,Android系统没有直接使用Linux消息队列来进行IPC的场景,但是有大量的场景都利用了消息队列的特性来设计通信方案,比如我们最频繁使用的Handler,就是一个消息队列


3)共享内存在Android中的应用

共享内存在Android系统中主要的使用场景是用来传输大数据,并且Android并没有直接使用Linux原生的共享内存方式,而是设计了Ashmem匿名共享内存。


4)socket在Android中的应用

zygote:用于孵化进程,system_server创建进程是通过socket向zygote进程发起请求;

installd:用于安装App的守护进程,上层PackageManagerService很多实现最终都是交给它来完成;

lmkd:lowmemorykiller的守护进程,Java层的LowMemoryKiller最终都是由lmkd来完成;

adbd:这个也不用说,用于服务adb;

logcatd:这个不用说,用于服务logcat;

vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。



请使用浏览器的分享功能分享到微信等