近期面试记录 (3)
4月7号面试时遇到一个问题:Android 中哪些跨进程通信场景没有使用 Binder ?Android 跨进程通信言必称 Binder,忽然脑洞大开一下”找不同”,看看哪里没有用 Binder,挺有意思的。
Binder 是 Android 系统最常见的进程间通信(IPC)方式,大量用于 Android 系统服务中,它是典型的 CS 架构。Binder 使用 Parcel 传递方法参数。
创建应用进程
启动 Activity 时,如果对应的应用进程不存在, ActivityManagerService (AMS) 会通过 LocalSocket 请求 Zygote 进程以 fork 方式创建一个新的应用进程。这个场景中 AmS 没有使用 Binder 。
省略部分细节之后,创建应用进程的大体流程如下:
这个流程中涉及到重要的类包括(部分类在图中未展示):
- AmS
- com.android.server.am.ProcessList
- ZygoteProcess
- android.os.AppZygote
- android.os.Process
涉及到的代码比较多,但最终会走到 ZygoteProcess.start()
方法,这个方法执行以下操作:
- 添加相关参数到
argsForZygote
- 使用 LocalSocket 跟 zygote 进程建立连接
- 将参数发送到 zygote 进程
1 | /** |
处理 Input Event
WmS 处理 Input Event 的流程中用到了 Pipe。具体来说就是 InputReader
往 Pipe 写入事件,InputDispatcher
从 Pipe 读取事件。
Pipe 是一种不同于 Binder 的 IPC 方式。不过在 WmS 处理 Input Event 这个场景中,Pipe 并非用于进程间通信,而是用于同一进程内两个不同线程之间的通信。(所以我面试时将这个场景中的 Pipe 作为一种不同于 Binder 的 IPC,其实搞错了。)
大体流程如下:
注意区分图中的线程与进程:
- InputReader 和 InputDispatcher 是同一个进程(system_server)中的不同线程
- InputReader 线程和 InputDispatcher 线程使用 Pipe 进行通信
- system_server 进程使用 Binder 跟应用进程通信
这个流程中涉及到重要的类包括(部分类在图中未展示):
- InputReader
- InputDispatcher
- InputChannel (对 Pipe 的封装)
- WindowManagerService
- ViewRootImpl
- PhoneWindow
共享图形缓冲区
SurfaceFlinger 与应用程序共享图形缓冲区。也就是说,共享内存是 SurfaceFlinger 跟应用进程进行交互的一种 IPC 机制。
应用程序将其用户界面渲染到内存缓冲区(通常由 GraphicBuffer 对象表示)中。这些缓冲区包含应用程序窗口或表面的像素数据。SurfaceFlinger 需要访问可能来自多个不同应用程序进程的这些缓冲区,以便将它们合成为屏幕上显示的最终图像。
图形缓冲区很大(通常为兆字节)。如果使用 Binder 等其他 IPC 机制(涉及序列化和数据复制),将整个缓冲区从应用程序的进程内存复制到 SurfaceFlinger 进程的内存,每帧的速度会非常慢,效率也会很低,从而导致 UI 性能低下和功耗过高。
所以这一场景中最合理的 IPC 方案是 SurfaceFlinger 与应用进程之间共享内存。不过实际中 SurfaceFlinger 和应用进程的 IPC 结合了共享内存和 Binder。大体流程如下:
注意上图中 Binder 和共享内存的不同作用:
- Binder: 用于协调、发送元数据和传递代表共享缓冲区的句柄(文件描述符)。
- 共享内存: 用于实际的大容量像素数据,允许生产者(应用程序)和消费者(SurfaceFlinger)之间的零拷贝访问。
总结
以上提到的几种不同的 IPC 方式总结如下:
延伸问题
AmS 和 Zygote 之间的 IPC 为什么没有使用 Binder?
Zygote 的设计目标
首先要说的是 Zygote 的角色。Zygote 进程是系统启动期间第一批启动的 native 进程之一。它的主要作用是初始化 Android Runtime(ART/Dalvik VM)、预加载核心类和资源,然后进入等待状态准备接收命令:以 fork()
方式高效地创建新的应用进程。
第二点是最小化启动开销。为了保证可以快速 fork 进程,以及新创建的应用进程少占用内存,Zygote 被设计得尽可能简单,避免加载不必要的库或启动复杂服务。
第三点是 Binder 的复杂性。使用 Binder Server 需要链接 Binder 库、初始化 Binder 相关的基础设施(线程池等等),可能还要到 servicemanager
那里注册相关信息。这会给 Zygote 带来过大的复杂性和开销,跟它”简单快速”的核心目标冲突。而 local socket 是一个简单得多的标准的 Linux IPC 机制,只需要很小的开销。
启动依赖问题
Zygote 的启动时机非常早,早于很多核心系统服务。在这个时间点,Binder 的某些基础设施可能尚未准备就绪。如果让 Zygote 依赖 Binder,会给启动时序带来复杂的依赖问题。
Local socket 是 Linux 内核的基础部分,在系统启动早期就可使用,没有复杂的依赖问题。(Local socket 是标准的 Linux IPC 机制,Binder 不是标准的 Linux IPC,它是 Android 特有)
交互的复杂程度
AmS 和 Zygote 的交互相对简单:使用指定 uid 和 gid 等参数 fork 一个新的进程。所以实际上只需要一个命令通道即可。
Binder 专为更丰富的面向对象 RPC 而设计,包括传递复杂对象、管理对象生命周期、处理回调以及基于调用者身份的细粒度权限检查。这比简单的 fork 请求所需的功能要多得多。使用 Binder 就像使用一个复杂的消息队列系统来发送一个命令字。通俗点说就是杀鸡焉用牛刀。
安全性
Zygote socket 受标准的 Linux 文件系统权限保护,只有已授权用户( AmS 运行在 system_server
,对应的用户是 system
)才能连接到这个 socket。所以在这个特定的系统内部交互过程中 local socket 的安全性是足够的。从这个角度来说,虽然 Binder 的安全机制更完善,但在这个场景并不来带来更多实质性的好处。
综上,使用 local socket 进行 AMS 到 Zygote 的通信是一种务实的选择,对于一个以快速创建进程为主要任务的早期启动的进程来说,我们更倾向于简单、低开销、最小依赖和高效。Binder 功能虽强大,但会为这个任务带来不必要的复杂性和开销。