本文所述的最佳实践可作为帮助您有效开发 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_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-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. 何时使用
在 Java 中,可以通过 @JavaOnlyStableParcelable
使用非结构化 Parcelable;在 NDK 后端,可以通过 @NdkOnlyStableParcelable
使用非结构化 Parcelable。这些通常是旧的和现有的 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;