AIDL 样式指南

此处概述的最佳实践作为有效开发 AIDL 接口的指南,并关注接口的灵活性,尤其是在 AIDL 用于定义 API 或与 API 表面交互时。

当应用需要在后台进程中相互交互或需要与系统交互时,可以使用 AIDL 定义 API。 有关在应用中使用 AIDL 开发编程接口的更多信息,请参阅Android 接口定义语言 (AIDL)。 有关 AIDL 实践的示例,请参阅HAL 的 AIDL稳定 AIDL

版本控制

AIDL API 的每个向后兼容快照都对应一个版本。 要拍摄快照,请运行 m <module-name>-freeze-api。 每当发布 API 的客户端或服务器时(例如,在 Mainline 版本中),您都需要拍摄快照并创建一个新版本。 对于系统到供应商 API,这应该在每年的平台修订时发生。

有关允许的更改类型的更多详细信息和信息,请参阅接口版本控制

API 设计指南

通用

1. 记录所有内容

  • 记录每个方法的语义、参数、内置异常的使用、服务特定异常和返回值。
  • 记录每个接口的语义。
  • 记录枚举和常量的语义含义。
  • 记录对于实施者可能不清楚的任何内容。
  • 在相关的地方提供示例。

2. 命名大小写

类型使用大驼峰命名法,方法、字段和参数使用小驼峰命名法。 例如,parcelable 类型使用 MyParcelable,参数使用 anArgument。 对于首字母缩略词,请将首字母缩略词视为一个单词 (NFC -> Nfc)。

[-Wconst-name] 枚举值和常量应为 ENUM_VALUECONSTANT_NAME

接口

1. 命名

[-Winterface-name] 接口名称应以 I 开头,例如 IFoo

2. 避免使用基于 ID 的“对象”的大型接口

当有许多与特定 API 相关的调用时,首选子接口。 这提供了以下好处

  • 使客户端或服务器代码更易于理解
  • 使对象的生命周期更简单
  • 利用 binder 不可伪造的特性。

不推荐: 具有基于 ID 的对象的单个大型接口

interface IManager {
   int getFooId();
   void beginFoo(int id); // clients in other processes can guess an ID
   void opFoo(int id);
   void recycleFoo(int id); // ownership not handled by type
}

推荐: 单独的接口

interface IManager {
    IFoo getFoo();
}

interface IFoo {
    void begin(); // clients in other processes can't guess a binder
    void op();
}

3. 不要将单向方法与双向方法混合使用

[-Wmixed-oneway] 不要将单向方法与非单向方法混合使用,因为这会使客户端和服务器理解线程模型变得复杂。 具体而言,在读取特定接口的客户端代码时,您需要查找每个方法以确定该方法是否会阻塞。

4. 避免返回状态代码

方法应避免使用状态代码作为返回值,因为所有 AIDL 方法都具有隐式状态返回代码。 请参阅 ServiceSpecificExceptionEX_SERVICE_SPECIFIC。 按照惯例,这些值在 AIDL 接口中定义为常量。 更多详细信息请参见 AIDL 后端的错误处理部分

5. 将数组作为输出参数被认为是有害的

[-Wout-array] 具有数组输出参数的方法(例如 void foo(out String[] ret))通常是不好的,因为输出数组大小必须由 Java 中的客户端声明和分配,因此服务器无法选择数组输出的大小。 发生这种不良行为的原因是数组在 Java 中的工作方式(它们无法重新分配)。 相反,首选类似 String[] foo() 的 API。

6. 避免使用 inout 参数

[-Winout-parameter] 这可能会使客户端感到困惑,因为即使是 in 参数也看起来像 out 参数。

7. 避免使用 out 和 inout @nullable 非数组参数

[-Wout-nullable] 由于 Java 后端不处理 @nullable 注解,而其他后端则处理,因此 out/inout @nullable T 可能会导致跨后端行为不一致。 例如,非 Java 后端可以将 out @nullable 参数设置为 null(在 C++ 中,将其设置为 std::nullopt),但 Java 客户端无法将其读取为 null。

结构化 Parcelable

1. 何时使用

当您有多种数据类型要发送时,请使用结构化 Parcelable。

或者,当您只有一种数据类型,但您预计将来需要扩展它时。 例如,不要使用 String username。 使用可扩展的 Parcelable,如下所示

parcelable User {
    String username;
}

这样,将来您可以按如下方式扩展它

parcelable User {
    String username;
    int id;
}

2. 显式提供默认值

[-Wexplicit-default, -Wenum-explicit-default] 为字段提供显式默认值。

非结构化 Parcelable

1. 何时使用

非结构化 Parcelable 在 Java 中通过 @JavaOnlyStableParcelable 提供,在 NDK 后端中通过 @NdkOnlyStableParcelable 提供。 通常,这些是旧的且现有的无法结构化的 Parcelable。

常量和枚举

1. 位域应使用常量字段

位域应使用常量字段(例如,接口中的 const int FOO = 3;)。

2. 枚举应为封闭集。

枚举应为封闭集。 注意:只有接口所有者可以添加枚举元素。 如果供应商或 OEM 需要扩展这些字段,则需要替代机制。 在可能的情况下,应优先考虑上游供应商功能。 但是,在某些情况下,可能允许自定义供应商值通过(尽管供应商应具有对该值进行版本控制的机制,可能是 AIDL 本身,它们不应相互冲突,并且这些值不应暴露给第三方应用)。

3. 避免使用类似“NUM_ELEMENTS”的值

由于枚举是版本化的,因此应避免使用指示存在多少个值的值。 在 C++ 中,可以使用 enum_range<> 来解决此问题。 对于 Rust,请使用 enum_values()。 在 Java 中,目前还没有解决方案。

不推荐: 使用编号值

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. 避免冗余前缀和后缀

[-Wredundant-name] 避免在常量和枚举器中使用冗余或重复的前缀和后缀。

不推荐: 使用冗余前缀

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

推荐: 直接命名枚举

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] 非常不鼓励使用 FileDescriptor 作为 AIDL 接口方法的参数或返回值。 尤其是在 Java 中实现 AIDL 时,这可能会导致文件描述符泄漏,除非小心处理。 基本上,如果您接受 FileDescriptor,则需要在不再使用它时手动关闭它。

对于原生后端,您是安全的,因为 FileDescriptor 映射到 unique_fd,后者是自动关闭的。 但无论您使用哪种后端语言,明智的做法是根本不要使用 FileDescriptor,因为这将限制您将来更改后端语言的自由。

相反,请使用 ParcelFileDescriptor,它是自动关闭的。

变量单位

确保变量单位包含在名称中,以便它们的单位得到明确定义和理解,而无需参考文档

示例

long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good

double energy; // Bad
double energyMilliJoules; // Good

int frequency; // Bad
int frequencyHz; // Good

时间戳必须指示其参考

时间戳(实际上,所有单位!)必须清楚地指示其单位和参考点。

示例

/**
 * Time since device boot in milliseconds
 */
long timestampMs;

/**
 * UTC time received from the NTP server in units of milliseconds
 * since January 1, 1970
 */
long utcTimeMs;