Android Rust IPC 调研

 

背景

在 Android 应用开发中,使用多进程,为不同服务创建独立进程是非常常见的。比如推送、保活、WebView、内存不够等优化策略。 目前 Android IPC 均是在 Java 层处理,随着 Android、iOS 一些底层组件、通用业务等使用 Rust 跨平台开发,为了更好的支持业务开发,因此调研已提供 Rust 侧 IPC 能力。

IPC 目的

在不同的进程之间安全有效地共享数据和消息,从而实现进程间的协作和数据交换。 alt text

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 传输数据

alt text

Binder 传输数据

alt text

小结

与传统 IPC 相比,Binder 更新符合我们的应用场景,不仅性能较好,且易用性高。 因此本次 IPC 采用 Binder 方式。

如何使用 Rust 实现

通过查阅官方相关文档资料,目前有以下三种具有可行性的方案:

  1. Rust 通过 JNI 调用 Java Binder 高级接口:该方式实现比较简单,但需要通过 JNI 调用到 Java 层,产生多次数据序列化
  2. 调用底层 C++ Binder API,使用 Rust 进行二层封装:这些 API 定义在 Android NDK 中,实现难度较高,需要手动管理内存、处理线程和进程间通信的细节。
  3. 使用 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 测试,筛选出两种可执行方案:

  1. 使用 Java Binder 编写 IPC,Rust 通过 JNI,实现 Rust 侧的 IPC。 alt text
    • 优点:实现比较简单,易用性高,Java-Java IPC 性能比较好
    • 缺点:仅支持 Android,Rust-Rust IPC 中间传递数据会多次数据序列化和反序列化
  2. 在 Rust 侧实现基于 UDS(Unix Domain Sockets)的 IPC。Java 侧 IPC 需求用过 StarBridge 调用 Rust 来实现 IPC。 alt text
    • 优点:直接支持于 Rust 标准库,相较其他方式使用简单,跨进程通信效率高,支持类 Linux 系统
    • 缺点:不具备 Binder 机制那样的高级特性,需要自己实现。为提高业务易用性,需要封装 Tokio 异步 IO 和事件循环,制定通信协议,及多 UDS 客户端支持等,开发周期相较长一些

对于两种方案的选择考虑:

  • 对于鸿蒙系统,目前没有确切的资料表明是否会支持多进程。
  • 目前多进程场景大部分使用在上层业务侧,Java 层是处理终点。 因此目前阶段采用【方案 1】是更合适的选择。

相关资料: