背景
在 Android 应用开发中,使用多进程,为不同服务创建独立进程是非常常见的。比如推送、保活、WebView、内存不够等优化策略。 目前 Android IPC 均是在 Java 层处理,随着 Android、iOS 一些底层组件、通用业务等使用 Rust 跨平台开发,为了更好的支持业务开发,因此调研已提供 Rust 侧 IPC 能力。
IPC 目的
在不同的进程之间安全有效地共享数据和消息,从而实现进程间的协作和数据交换。
IPC 机制有哪些
管道(Pipes)、消息队列(Message Queues)、共享内存(Shared Memory)、信号量(Semaphores)、套接字(Sockets)、文件、Binder(Android)、广播(Android)等
IPC Bench
评估了 Linux 生态系统中各种 IPC 通信的性能基准,测量方式是通过在两个进程之间发送 ping-pong 消息
Method | 100 Byte Messages | 1 Kilo Byte Messages |
---|---|---|
Unix Signals | –broken– | –broken– |
ZeroMQ (TCP) | 24,901 msg/s | 22,679 msg/s |
Internet sockets (TCP) | 70,221 msg/s | 67,901 msg/s |
Domain sockets | 130,372 msg/s | 127,582 msg/s |
Pipes | 162,441 msg/s | 155,404 msg/s |
Message Queues | 232,253 msg/s | 213,796 msg/s |
FIFOs (named pipes) | 265,823 msg/s | 254,880 msg/s |
Shared Memory | 4,702,557 msg/s | 1,659,291 msg/s |
Memory-Mapped Files | 5,338,860 msg/s | 1,701,759 msg/s |
Binder 与传统 IPC 对比
Binder | 共享内存 | Socket | |
---|---|---|---|
性能 | 需要拷贝一次 | 无需拷贝 | 需要拷贝两次 |
特点 | 基于 C/S 架构 易用性高 | 控制复杂 易用性差 | 基于 C/S 架构 作为一款通用接口,传输效率低,开销大 |
安全性 | 为每个 App 分配 UID 同时支持实名和匿名 | 依赖上层协议 访问接入点是开放的 不安全 | 依赖上层协议 访问接入点是开放的 不安全 |
传统 IPC 传输数据
Binder 传输数据
小结
与传统 IPC 相比,Binder 更新符合我们的应用场景,不仅性能较好,且易用性高。 因此本次 IPC 采用 Binder 方式。
如何使用 Rust 实现
通过查阅官方相关文档资料,目前有以下三种具有可行性的方案:
- Rust 通过 JNI 调用 Java Binder 高级接口:该方式实现比较简单,但需要通过 JNI 调用到 Java 层,产生多次数据序列化
- 调用底层 C++ Binder API,使用 Rust 进行二层封装:这些 API 定义在 Android NDK 中,实现难度较高,需要手动管理内存、处理线程和进程间通信的细节。
- 使用 Android 构建系统(Soong)通过 AIDL 构建 Rust Library:随着 Rust 的被广泛应用,Google 官方 Android 团队开发的免费 Rust 课中有提及到 Rust Binder 的使用。需要搭建 AOSP 开发平台环境进行测试开发。
Android 构建系统(Soong)
Android 构建系统(Soong)通过一系列模块来支持 Rust:
Module Type | 描述 |
---|---|
rust_binary | Produces a Rust binary. |
rust_library | 生成一个 Rust 库,并提供 rlib 和 dylib 两种变体。 |
rust_ffi | 生成一个可由 cc 模块使用的 Rust C 库,并提供静态和共享两种变体。 |
rust_proc_macro | 生成“proc-macro”Rust 库。这些宏与编译器插件类似。 |
rust_test | 生成使用标准 Rust 测试框架的 Rust 测试二进制文件。 |
rust_fuzz | 生成使用 libfuzzer 的 Rust 模糊测试二进制文件。 |
rust_protobuf | 生成源代码并生成为特定 protobuf 提供接口的 Rust 库。 |
rust_bindgen | 生成源代码并生成包含 Rust 绑定到 C 库的 Rust 库。 |
由于 AIDL Rust 后端是在 Android 12 中引入的;NDK 后端从 Android 10 开始的。我们 App 最低支持到 Android 5,因此不能采用该方案。
其他 IPC 机制实现
使用 Unix 域套接字
Unix 套接字是一种在 Unix 或类 Unix 系统(包括 Android)上进行进程间通信的机制。Rust 标准库支持 Unix 套接字(std::os::unix::net),使其成为 Rust 应用程序之间进行 IPC 的一个简单而强大的选择。
- 优点: 直接支持于 Rust 标准库,使用简单,跨进程通信效率高。
- 缺点: 不具备 Binder 机制那样的高级特性,如跨语言调用、远程过程调用(RPC)等。
Rust 相关组件库 uds
使用共享内存
共享内存是一种高效的 IPC 机制,进程可以通过将内存段映射到它们的地址空间来共享数据。在 Rust 中,可以使用 mmap 库来实现共享内存。
- 优点: 非常高效,适用于大量数据交换。
- 缺点: 编程复杂度高,需要手动管理同步和数据一致性。
使用消息队列
消息队列提供了一种松散耦合的进程间通信方式,进程可以发送和接收消息,无需关心对方的具体实现。
- 优点: 简化了进程间的数据传递,易于使用和理解。
- 缺点: 可能不如直接使用 Binder 或 Unix 套接字那样高效。
- 在 Rust 中,有 ipc-channel 库,但是在 Android 中支持暂不完善。
使用 DBus
DBus 是一种在 Unix 和类 Unix 系统中广泛使用的消息总线系统,它提供了一种机制,允许不同的应用程序和服务之间进行通信。DBus 支持两种主要类型的总线:
- 系统总线(System bus): 用于系统级服务之间的通信。
- 会话总线(Session bus): 用于用户会话内应用程序之间的通信。
在技术上,可以在 Android 上使用 DBus 进行 IPC,由于它不是 Android 系统的一部分,使用 DBus 可能会遇到兼容性问题,需要进行完整的测试。 Rust 组件库 dbus-rs
总结
经过编写 Demo 测试,筛选出两种可执行方案:
- 使用 Java Binder 编写 IPC,Rust 通过 JNI,实现 Rust 侧的 IPC。
- 优点:实现比较简单,易用性高,Java-Java IPC 性能比较好
- 缺点:仅支持 Android,Rust-Rust IPC 中间传递数据会多次数据序列化和反序列化
- 在 Rust 侧实现基于 UDS(Unix Domain Sockets)的 IPC。Java 侧 IPC 需求用过 StarBridge 调用 Rust 来实现 IPC。
- 优点:直接支持于 Rust 标准库,相较其他方式使用简单,跨进程通信效率高,支持类 Linux 系统
- 缺点:不具备 Binder 机制那样的高级特性,需要自己实现。为提高业务易用性,需要封装 Tokio 异步 IO 和事件循环,制定通信协议,及多 UDS 客户端支持等,开发周期相较长一些
对于两种方案的选择考虑:
- 对于鸿蒙系统,目前没有确切的资料表明是否会支持多进程。
- 目前多进程场景大部分使用在上层业务侧,Java 层是处理终点。 因此目前阶段采用【方案 1】是更合适的选择。
相关资料: