AIDL 样式指南

本文所述的最佳实践可作为帮助您有效开发 AIDL 接口的指南,并且注重接口的灵活性,尤其是在 AIDL 用于定义 API 或与 API Surface 交互时。

当应用需要在后台进程中彼此交互或需要与系统交互时,AIDL 可用于定义 API。如需详细了解如何使用 AIDL 开发应用中的编程接口,请参阅 Android 接口定义语言 (AIDL)。如需查看 AIDL 实践中的示例,请参阅适用于 HAL 的 AIDL稳定的 AIDL

版本控制

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

如需了解详情以及有关允许的变更类型的信息,请参阅对接口进行版本编号

API 设计准则

常规

1. 记录所有要素

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

2. 大小写

针对类型使用大驼峰命名法,而针对方法、字段和参数使用小驼峰命名法。例如,MyParcelable 用于 Parcelable 类型,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-param] 这可能会使客户端感到困惑,因为即使是 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;