此处概述的最佳实践作为有效开发 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_VALUE
和 CONSTANT_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 方法都具有隐式状态返回代码。 请参阅 ServiceSpecificException
或 EX_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;