服务和数据传输

本页介绍了如何注册和发现服务,以及如何通过调用 .hal 文件内的接口中定义的方法将数据发送到服务。

注册服务

HIDL 接口服务器(实现接口的对象)可注册为已命名的服务。注册的名称不需要与接口或软件包名称相关。如果没有指定名称,则使用名称“默认”;对于不需要注册同一接口的两个实现的 HAL,应使用该名称。例如,在每个接口中定义的服务注册的 C++ 调用是:

status_t status = myFoo->registerAsService();
status_t anotherStatus = anotherFoo->registerAsService("another_foo_service");  // if needed

HIDL 接口的版本包含在接口本身中。版本自动与服务注册关联,并可通过每个 HIDL 接口上的方法调用 (android::hardware::IInterface::getInterfaceVersion()) 进行检索。服务器对象不需要注册,并可通过 HIDL 方法参数传递到其他进程,相应的接收进程会向服务器发送 HIDL 方法调用。

发现服务

客户端代码按名称和版本请求给定的接口,并对所需的 HAL 类调用 getService

// C++
sp<V1_1::IFooService> service = V1_1::IFooService::getService();
sp<V1_1::IFooService> alternateService = V1_1::IFooService::getService("another_foo_service");
// Java
V1_1.IFooService service = V1_1.IFooService.getService(true /* retry */);
V1_1.IFooService alternateService = V1_1.IFooService.getService("another", true /* retry */);

每个版本的 HIDL 接口都会被视为单独的接口。因此,IFooService 版本 1.1 和 IFooService 版本 2.2 都可以注册为“foo_service”,在任一接口上调用 getService("foo_service") 都会获得相应接口的已注册服务。因此,在大多数情况下,注册或发现服务均无需提供名称参数(也就是说名称为“默认”)。

供应商接口对象还会影响所返回接口的传输方法。对于软件包 android.hardware.foo@1.0 中的接口 IFooIFoo::getService 返回的接口始终使用设备清单中针对 android.hardware.foo 声明的传输方法(如果相应条目存在的话);如果该传输方法不可用,则返回 nullptr。

在某些情况下,即使没有获得相应服务,也可能需要立即继续。例如,当客户端希望自行管理服务通知或者在需要获得所有 hwservice 并检索它们的诊断程序(如 atrace)中时,可能会发生这种情况。在这种情况下,可以使用其他 API,如 C++ 中的 tryGetService 或 Java 中的 getService("instance-name", false)。Java 中提供的旧版 API getService 也必须与服务通知一起使用。使用此 API 无法避免以下竞态条件:当客户端使用某个非重试 API 请求某个服务器后,该服务器对自身进行注册。

服务终止通知

想要在服务终止时收到通知的客户端会接收到框架传送的终止通知。如需接收通知,客户端必须:

  1. 创建 HIDL 类/接口 hidl_death_recipient 的子类(在 C++ 代码中,而不是在 HIDL 中)。
  2. 替换其 serviceDied() 方法。
  3. 实例化 hidl_death_recipient 子类的对象。
  4. 在要监控的服务上调用 linkToDeath() 方法,并传入 IDeathRecipient 的接口对象。请注意,此方法并不具备其调用方所在的终止接收方或代理的所有权。

伪代码示例(C++ 和 Java 类似):

class IMyDeathReceiver : hidl_death_recipient {
  virtual void serviceDied(uint64_t cookie,
                           wp<IBase>& service) override {
    log("RIP service %d!", cookie);  // Cookie should be 42
  }
};
....
IMyDeathReceiver deathReceiver = new IMyDeathReceiver();
m_importantService->linkToDeath(deathReceiver, 42);

同一终止接收方可能已在多个不同的服务上注册。

数据传输

可通过调用 .hal 文件内的接口中定义的方法将数据发送到服务。具体方法有两类:

  • 阻塞方法会等到服务器产生结果。
  • Oneway方法仅会朝一个方向发送数据,并且不会阻塞。如果 RPC 调用中正在传输的数据量超过实现限制,调用可能会阻塞,也可能会返回错误指示(具体行为尚不确定)。

不返回值但未声明为 oneway 的方法仍会阻塞。

在 HIDL 接口中声明的所有方法都在一个方向上调用,要么从 HAL 发出,要么到 HAL。接口没有指定具体调用方向。需要从 HAL 发起调用的架构应该在 HAL 软件包中提供两个(或更多个)接口并从每个进程提供相应的接口。我们根据接口的调用方向来取名“客户端”或“服务器”(即 HAL 可以是一个接口的服务器,也可以是另一个接口的客户端)。

回调

“回调”一词可以指代两个不同的概念,可通过“同步回调”和“异步回调”进行区分。

“同步回调”用于返回数据的一些 HIDL 方法。返回多个值(或返回非基元类型的一个值)的 HIDL 方法会通过回调函数返回其结果。如果只返回一个值且该值是基元类型,则不使用回调,并且该值是从方法返回的。服务器实现 HIDL 方法,而客户端实现回调。

“异步回调”允许 HIDL 接口的服务器发起调用。通过第一个接口传递第二个接口的实例即可完成此操作。第一个接口的客户端必须作为第二个接口的服务器。第一个接口的服务器可以在第二个接口对象上调用方法。例如,HAL 实现可以在使用它的进程创建和提供的接口对象上调用方法,将信息异步发送回进程。用于异步回调的接口中的方法可以是阻塞(并且可以将值返回到调用方),也可以是 oneway。如需查看相关示例,请参阅 HIDL C++ 中的“异步回调”。

为了简化内存所有权,方法调用和回调只接受 in 参数,而不支持 outinout 参数。

每事务限制

对于 HIDL 方法和回调中发送的数据量,未施加每事务限制。但是,每事务调用 4KB 以上的数据便被视为过度调用。如果发生这种情况,建议重新设计给定 HIDL 接口的架构。另一个限制是可供 HIDL 基础架构处理多个同时进行的事务的资源。如果有多个线程或进程向一个进程发送调用,或者接收进程无法快速处理多个 oneway 调用,则可能会有多个事务同时进行。默认情况下,所有并发事务可用的最大总空间为 1MB。

在设计良好的接口中,不应出现超出这些资源限制的情况;如果超出的话,超出资源的调用可能会阻塞,直到资源可用或发出提示传输错误的信号。每当因正在进行的总事务导致出现超出每事务限制或溢出 HIDL 实现资源的情况时,系统都会记录下来以方便调试。

方法实现

HIDL 生成以目标语言(C++ 或 Java)声明必要类型、方法和回调的标头文件。客户端和服务器代码的 HIDL 定义方法和回调的原型是相同的。HIDL 系统提供调用程序端(整理 IPC 传输的数据)的方法代理实现,并将代码存根到被调用程序端(将数据传递到方法的开发者实现)。

函数的调用方(HIDL 方法或回调)拥有对传递到相应函数的数据结构的所有权,并在调用后保留所有权;在所有情况下,被调用方都无需释放存储空间。

  • 在 C++ 中,数据可能是只读的(尝试写入可能会导致细分错误),并且在调用期间有效。客户端可以深层复制数据,以在调用期间外传播。
  • 在 Java 中,代码会接收数据的本地副本(普通 Java 对象),然后它们可以保留和修改此数据,也可以允许垃圾回收器将其回收。

非 RPC 数据传输

HIDL 在不使用 RPC 调用的情况下通过两种方法来传输数据:一是共享内存;二是快速消息队列 (FMQ)。只有 C++ 支持这两种方法。

  • 共享内存。内置 HIDL 类型 memory 用于传递表示已分配的共享内存的对象。可以在接收进程中用来映射共享内存。
  • 快速消息队列 (FMQ)。HIDL 提供了一种可实现无等待消息传递的模板化消息队列类型。它在透传模式或绑定模式下不使用内核或调度程序(设备间通信将不具备这些属性)。通常,HAL 会设置队列的一端,从而创建一个可借助内置 HIDL 类型 MQDescriptorSyncMQDescriptorUnsync 的参数通过 RPC 传递的对象。接收进程可使用此对象设置队列的另一端。
    • “已同步”队列不能溢出,且只能有一个读取器。
    • “未同步”队列可以溢出,且可以有多个读取器;每个读取器必须及时读取数据,否则数据就会丢失。
    两种队列都不能下溢(从空队列进行读取将会失败),且都只能有一个写入器。

如需详细了解 FMQ,请参阅快速消息队列 (FMQ)