Android核心原理 5.3
声明
- 在Android系统中经常会遇到一些系统原理性的问题,在此专栏中集中来讨论下。
- Android系统主要由Java和C/C++两个世界构成,此篇分析处于Java世界和C/C++世界的两个进程如何实现进程间通信的问题。
- 此篇参考一些博客和书籍,代码基于Android 9.0.0,不方便逐一列出,仅供学习、知识分享。
1 Native程序的结构设计
Android 的本地程序基于 Linux 中标准 C和C++的开发,这部分内容和其他系统的思路相同比较特殊的地方是Android 的本地程序使用 Android 中的通信机制,由此也产生了特殊的程序结构。实际上Android 很少有只需要在本地处理而不需要和 Java 层进行任何交互的程序。
2 设计思路
Android 系统中很多功能的实现,通常需要增加一个部分的可执行程序作为守护进程。此守护进程不但有一个循环运行,又能与外部进行交互。Android 系统中需要有本地的守护进程,主要由于以下的原因
- 在Android 系统实现某些功能的时候,需要一个本地的循环在后台运行,并统一管理一些全局的内容。
- Android 中有某些部件是从其他系统移植过来的,本身守护进程(Daemon)的形式。
Android 需要保持这种结构。这种守护进程本质是本地的可执行程序,因此它可以调用 Linux 的驱动程序,调用很多本地库来实现它的功能。在构建一个 Android 中的本地守护进程的时候,核心问题是它如何与Java层进程进行交互?本质上是通信的问题。不仅需要考虑与其他本地程序通信的手段,也需要考虑 Java 程序通信的手段。
Android 的本地守护进程的结构设计主要具有两种典型的形式:
- 使用 保留的 Socket 机制。
- 使用 Binder 机制。
它们都是使用守护进程实现具体的功能,并且具有不同形式的“交互者”。在权限问题上,本地的守护进程是一个权限,与它交互的程序又可以是另外的一个权限。
3 守护进程 + 保留 Socket 的设计结构
Android 中的本地守护进程,需要统一在init.rc 文件中将可执行程序声明成服务,然后由 init 程序解析脚本运行它们。在声明服务的时候,可以赋予它一个 Socket 作为对外通信的手段。
Android 中保留的套接字对应 /dev/socket 中的一个套接字文件。在本地层和 Java 层都具有操作这种套接字的接口。在这种典型的程序结构中,本地的守护进程,可以访问本地驱动,可以调用其他的本地库。这与程序的具体实现相关,而和程序通信结构无关。
通过 init.rc 为守护进程建立了保留的 Socket。从守护进程的角度,得到这个套接字之后,可以再对其中内容进行解析。需要与这个守护进程进行交互的程序可以有不同的几种调用方式:
- 本地的库可以通过本地 Socket 方法访问此 Socket,进一步还可以封装一层JNI提供给 Java 层调用;
- Java 框架层中的类可以通过 Java 层的 Socket 方法访问此 Socket,进一步可以提供Java 类给 Java 应用层调用;
- Java 应用程序层也可以通过 Java 层的 Socket 方法直接访问此 Socket,此种方法的使用比较少;
以上的几种方式,都可以建立一条从本地的守护进程到 Java 层之间的通信通道。让 Java直接访问套接字,还可以避免使用JNI。
保留的 Socket 被打开后,本质上就是一个文件描述符,对其进行读/写可以表示控制操作或者数据传输。具体的协议需要由程序自行定义。保留的 Socket 还可以在 init.rc 中定义权限,原则是守护进程和其调用者都可以访问,限制别的程序对其访问。守护进程+保留 Socket 的结构一般适合守护进程在后台做的事情多,与外部交互较少的情况。Socket 提供的是表示命令的字符串,守护进程得到命令之后,执行调用者的请求,并可以有响应字符串返回。套接字的特点是异步的操作,请求和响应将是两个分离的阶段。Android 系统默认的实现中,几个全局的守护进程:vold、netd、installd、keystored 以及用于电话的 rild 均以这种程序结构存在。守护进程直接和 Java 层通过 Socket 通信,因此不涉及JNI 。
4 守护进程 + 本地层 Binder 的设计结构
此方式利用 Android主要的IPC 机制 Binder,可以进行更为灵活的跨进程的函数调用。在这种方式中,如果需要和 Java 层进行交互,必须使用利用JNI 对本地内容进行封装。
守护进程+本地层 Binder 实现方式的内容可以分成 3 部分:
-
- 具体 Binder IPC 的框架:它由IXXX、BnXXX和BpXXX 3个类构成,这是通用的基本结构。
-
- 实现者:主体的部分也就是 BnXXX 的继承者,它可以通过调用驱动程序或者本地的库来实现功能。守护进程就是 BnXXX 的继承者的运行载体,负责建立其实例并且增加到 servicemanager 当中。这个 BnXXX的承者的运行用户等上下文就是守护进程的上下文:类建立之后,可以在后台为自己运行线程;还可以等待客户端通过 Binder对其调用。
-
- 交互者,也就是 Binder 的不同层次的调用者:直接的调用关系是通过servicemanager 得到IXXX,并对其调用。为了调用的方便,通常将其封装成一个本地更适合调用的本地名称类似 XXX 形式的类,这个类就是这种框架本地调用的接口。将 XXX通过JNI封装成一个Java 中的XXX类作为Java 层调用的接口,可以为 Java 框架层或者 Java应用层调用。无论调用者调用的是本地或者 Java 的接口,它都运行于自己的进程中。
- 交互者,也就是 Binder 的不同层次的调用者:直接的调用关系是通过servicemanager 得到IXXX,并对其调用。为了调用的方便,通常将其封装成一个本地更适合调用的本地名称类似 XXX 形式的类,这个类就是这种框架本地调用的接口。将 XXX通过JNI封装成一个Java 中的XXX类作为Java 层调用的接口,可以为 Java 框架层或者 Java应用层调用。无论调用者调用的是本地或者 Java 的接口,它都运行于自己的进程中。
利用 Binder 的 IPC 机制调用本质上是同步调用,实现者返回后调用者才可以返回,因此与一个进程中的调用情况相似。一般适合与外部交互比较多的情况。这种远程函数调用(RPC)的交互方式显然更适合复杂的调用关系。Android 系统默认的实现中,media 守护进程中的MediaPlayerService、CameraService 和AudioFlinger 等类使用的是这种典型的结构,SurfaceFlinger 情况类似,但是默认情况下没有独立运行的守护进程。