Android 10 增加了对稳定的 Android 接口定义语言 (AIDL) 的支持,这是一种跟踪 AIDL 接口提供的应用程序接口 (API)/应用程序二进制接口 (ABI) 的新方法。稳定的 AIDL 与 AIDL 有以下主要区别:
- 接口在构建系统中使用
aidl_interfaces
定义。 - 接口只能包含结构化数据。代表所需类型的 Parcelables 是根据它们的 AIDL 定义自动创建的,并且会自动编组和解组。
- 接口可以声明为稳定的(向后兼容)。发生这种情况时,他们的 API 会在 AIDL 接口旁边的文件中进行跟踪和版本控制。
定义 AIDL 接口
aidl_interface
的定义如下所示:
aidl_interface {
name: "my-aidl",
srcs: ["srcs/aidl/**/*.aidl"],
local_include_dir: "srcs/aidl",
imports: ["other-aidl"],
versions_with_info: [
{
version: "1",
imports: ["other-aidl-V1"],
},
{
version: "2",
imports: ["other-aidl-V3"],
}
],
stability: "vintf",
backend: {
java: {
enabled: true,
platform_apis: true,
},
cpp: {
enabled: true,
},
ndk: {
enabled: true,
},
rust: {
enabled: true,
},
},
}
-
name
:AIDL接口模块的名称,唯一标识一个AIDL接口。 -
srcs
:构成接口的 AIDL 源文件列表。包com.acme
中定义的 AIDL 类型Foo
的路径应位于<base_path>/com/acme/Foo.aidl
,其中<base_path>
可以是与Android.bp
所在目录相关的任何目录。在上面的示例中,<base_path>
是srcs/aidl
。 -
local_include_dir
:包名称开始的路径。它对应于上面解释的<base_path>
。 -
imports
:它使用的aidl_interface
模块列表。如果您的一个 AIDL 接口使用了另一个aidl_interface
接口或 parcelable,请将其名称放在这里。这可以是名称本身,以引用最新版本,或带有版本后缀的名称(例如-V1
)以引用特定版本。从Android 12开始支持指定版本 versions
:冻结在api_dir
下的接口的先前版本,从 Android 11 开始,versions
冻结在aidl_api/ name
下。如果接口没有冻结版本,则不应指定,并且不会进行兼容性检查。对于 13 及更高版本,此字段已替换为versions_with_info
。-
versions_with_info
:元组列表,每个元组包含一个冻结版本的名称和一个列表,其中包含此版本的 aidl_interface 导入的其他 aidl_interface 模块的版本导入。 AIDL 接口 IFACE 的版本 V 的定义位于aidl_api/ IFACE / V
。该字段是Android 13引入的,不能直接在Android.bp中修改。通过调用*-update-api
或*-freeze-api
添加或更新该字段。此外,当用户调用*-update-api
或*-freeze-api
时,versions
字段会自动迁移到versions_with_info
。 -
stability
:此接口的稳定性承诺的可选标志。目前仅支持"vintf"
。如果未设置,则对应于在此编译上下文中具有稳定性的接口(因此此处加载的接口只能与一起编译的东西一起使用,例如在 system.img 上)。如果将其设置为"vintf"
,则这对应于稳定性承诺:接口必须在使用期间保持稳定。 -
gen_trace
:用于打开或关闭跟踪的可选标志。默认为false
。 -
host_supported
:可选标志,当设置为true
时,生成的库可用于主机环境。 -
unstable
:用于标记此接口不需要稳定的可选标志。设置为true
时,构建系统既不会为接口创建 API 转储,也不会要求对其进行更新。 -
frozen
:可选标志,当设置为true
时表示该接口自上一版本接口以来没有任何变化。这会启用更多构建时检查。当设置为false
时,这意味着接口正在开发中并且有新的更改,因此运行foo-freeze-api
将生成一个新版本并自动将值更改为true
。在 Android 14 中引入(AOSP 实验性)。 -
backend.<type>.enabled
:这些标志切换 AIDL 编译器为其生成代码的每个后端。目前支持四种后端:Java、C++、NDK 和 Rust。默认启用 Java、C++ 和 NDK 后端。如果不需要这三个后端中的任何一个,则需要明确禁用它。默认情况下禁用 Rust。 -
backend.<type>.apex_available
:生成的存根库可用于的 APEX 名称列表。 -
backend.[cpp|java].gen_log
:可选标志,用于控制是否生成附加代码以收集有关事务的信息。 -
backend.[cpp|java].vndk.enabled
:使该接口成为 VNDK 一部分的可选标志。默认为false
。 -
backend.java.sdk_version
:用于指定构建 Java 存根库所针对的 SDK 版本的可选标志。默认值为"system_current"
。当backend.java.platform_apis
为真时不应设置此项。 -
backend.java.platform_apis
:当生成的库需要针对平台 API 而不是 SDK 构建时,应设置为true
的可选标志。
对于版本和启用的后端的每个组合,都会创建一个存根库。特定后端如何引用特定版本的存根库,参见模块命名规则。
编写 AIDL 文件
稳定 AIDL 中的接口类似于传统接口,不同之处在于它们不允许使用非结构化的 parcelables(因为它们不稳定!)。稳定版 AIDL 的主要区别在于 parcelable 的定义方式。以前,parcelables 是预先声明的;在稳定的 AIDL 中,parcelables 字段和变量是明确定义的。
// in a file like 'some/package/Thing.aidl'
package some.package;
parcelable SubThing {
String a = "foo";
int b;
}
boolean
、 char
、 float
、 double
、 byte
、 int
、 long
和String
当前支持(但不是必需的)默认值。在 Android 12 中,还支持用户定义枚举的默认值。如果未指定默认值,则使用类似 0 或空值。没有默认值的枚举被初始化为 0,即使没有零枚举器也是如此。
使用存根库
将存根库作为依赖项添加到模块后,您可以将它们包含到文件中。以下是构建系统中存根库的示例( Android.mk
也可用于遗留模块定义):
cc_... {
name: ...,
shared_libs: ["my-module-name-cpp"],
...
}
# or
java_... {
name: ...,
// can also be shared_libs if desire is to load a library and share
// it among multiple users or if you only need access to constants
static_libs: ["my-module-name-java"],
...
}
# or
rust_... {
name: ...,
rust_libs: ["my-module-name-rust"],
...
}
C++ 中的示例:
#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
// use just like traditional AIDL
Java 中的示例:
import some.package.IFoo;
import some.package.Thing;
...
// use just like traditional AIDL
Rust 中的示例:
use aidl_interface_name::aidl::some::package::{IFoo, Thing};
...
// use just like traditional AIDL
版本控制接口
声明一个名为foo的模块还会在构建系统中创建一个目标,您可以使用它来管理模块的 API。构建时, foo-freeze-api在api_dir
或aidl_api/ name
下添加一个新的 API 定义,具体取决于 Android 版本,并添加一个.hash
文件,两者都代表接口的新冻结版本。 foo-freeze-api还更新了versions_with_info
属性以反映该版本的附加版本和imports
。基本上, versions_with_info
中的imports
是从imports
字段复制的。但是最新的稳定版本是在versions_with_info
的imports
中为没有明确版本的导入指定的。一旦指定了versions_with_info
属性,构建系统就会在冻结版本之间以及树顶 (ToT) 和最新冻结版本之间运行兼容性检查。
此外,您需要管理 ToT 版本的 API 定义。每当 API 更新时,运行foo-update-api来更新aidl_api/ name /current
,其中包含 ToT 版本的 API 定义。
为了保持接口的稳定性,所有者可以添加新的:
- 接口结束的方法(或具有明确定义的新序列号的方法)
- 元素到 parcelable 的末尾(需要为每个元素添加默认值)
- 常数值
- 在 Android 11 中,枚举器
- 在 Android 12 中,联合体末尾的字段
不允许其他操作,也没有其他人可以修改接口(否则他们可能会与所有者所做的更改发生冲突)。
要测试所有接口是否已冻结以供发布,您可以使用以下环境变量集进行构建:
-
AIDL_FROZEN_REL=true m ...
- 构建需要冻结所有稳定的 AIDL 接口,这些接口没有指定owner:
字段。 -
AIDL_FROZEN_OWNERS="aosp test"
- 构建要求所有稳定的 AIDL 接口与owner:
指定为“aosp”或“test”的字段。
进口稳定性
为接口的冻结版本更新导入版本在稳定的 AIDL 层是向后兼容的。然而,更新这些需要更新所有使用旧版本接口的服务器和客户端,并且一些应用程序在混合不同版本的类型时可能会混淆。通常,对于仅类型或通用包,这是安全的,因为需要已经编写代码来处理来自 IPC 事务的未知类型。
Android平台代码android.hardware.graphics.common
就是这种版本升级的最大例子。
使用版本化接口
接口方法
在运行时,当尝试在旧服务器上调用新方法时,新客户端会收到错误或异常,具体取决于后端。
-
cpp
后端获取::android::UNKNOWN_TRANSACTION
。 -
ndk
后端获取STATUS_UNKNOWN_TRANSACTION
。 -
java
后端获取android.os.RemoteException
并显示一条消息,说明 API 未实现。
包裹
当新字段添加到 parcelables 时,旧的客户端和服务器会丢弃它们。当新客户端和服务器收到旧的 parcelables 时,新字段的默认值会自动填写。这意味着需要为 parcelable 中的所有新字段指定默认值。
客户端不应该期望服务器使用新字段,除非他们知道服务器正在实现定义了字段的版本(请参阅查询版本)。
枚举和常量
同样,客户端和服务器应酌情拒绝或忽略无法识别的常量值和枚举器,因为将来可能会添加更多。例如,服务器在收到它不知道的枚举器时不应中止。它应该忽略它,或者返回一些东西,以便客户端知道它在此实现中不受支持。
工会
如果接收方是旧的并且不知道该字段,则尝试发送具有新字段的联合会失败。实现永远不会看到与新字段的联合。如果是单向事务,则忽略失败;否则错误是BAD_VALUE
(对于 C++ 或 NDK 后端)或IllegalArgumentException
(对于 Java 后端)。如果客户端将新字段的联合集发送到旧服务器,或者旧客户端从新服务器接收联合,则会收到错误。
模块命名规则
在 Android 11 中,对于启用的版本和后端的每个组合,都会自动创建一个存根库模块。要引用特定的存根库模块进行链接,不要使用aidl_interface
模块的名称,而是使用存根库模块的名称,即ifacename - version - backend ,其中
ifacename
:aidl_interface
模块的名称version
是V version-number
-
V latest-frozen-version-number + 1
树尖(尚未冻结)版本
backend
是java
用于 Java 后端,- 用于 C++ 后端的
cpp
, - NDK 后端的
ndk
或ndk_platform
。前者是针对应用,后者是针对平台使用, - Rust 后端的
rust
。
假设有一个名为foo的模块,其最新版本为2 ,并且它同时支持 NDK 和 C++。在这种情况下,AIDL 生成这些模块:
- 基于版本 1
-
foo-V1-(java|cpp|ndk|ndk_platform|rust)
-
- 基于版本 2(最新稳定版)
-
foo-V2-(java|cpp|ndk|ndk_platform|rust)
-
- 基于 ToT 版本
foo-V3-(java|cpp|ndk|ndk_platform|rust)
与安卓11相比,
-
foo- backend
,指的是最新的稳定版本变成foo- V2 - backend
-
foo-unstable- backend
,指的是 ToT 版本变成foo- V3 - backend
输出文件名始终与模块名相同。
- 基于版本 1:
foo-V1-(cpp|ndk|ndk_platform|rust).so
- 基于版本 2:
foo-V2-(cpp|ndk|ndk_platform|rust).so
- 基于 ToT 版本:
foo-V3-(cpp|ndk|ndk_platform|rust).so
请注意,AIDL 编译器不会为稳定的 AIDL 接口创建unstable
版本模块或非版本模块。从 Android 12 开始,从稳定的 AIDL 接口生成的模块名称始终包含其版本。
新的元接口方法
Android 10 为稳定的 AIDL 添加了几个元接口方法。
查询远程对象的接口版本
客户端可以查询远程对象正在实现的接口的版本和散列,并将返回值与客户端正在使用的接口的值进行比较。
cpp
后端示例:
sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
// the remote side is using an older interface
}
std::string my_hash = IFoo::HASH;
std::string remote_hash = foo->getInterfaceHash();
ndk
(和ndk_platform
)后端示例:
IFoo* foo = ... // the remote object
int32_t my_ver = IFoo::version;
int32_t remote_ver = 0;
if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
// the remote side is using an older interface
}
std::string my_hash = IFoo::hash;
std::string remote_hash;
foo->getInterfaceHash(&remote_hash);
java
后端示例:
IFoo foo = ... // the remote object
int myVer = IFoo.VERSION;
int remoteVer = foo.getInterfaceVersion();
if (remoteVer < myVer) {
// the remote side is using an older interface
}
String myHash = IFoo.HASH;
String remoteHash = foo.getInterfaceHash();
对于 Java 语言,远程端必须实现getInterfaceVersion()
和getInterfaceHash()
如下(使用super
而不是IFoo
以避免复制/粘贴错误。可能需要注释@SuppressWarnings("static")
来禁用警告,具体取决于javac
配置):
class MyFoo extends IFoo.Stub {
@Override
public final int getInterfaceVersion() { return super.VERSION; }
@Override
public final String getInterfaceHash() { return super.HASH; }
}
这是因为生成的类( IFoo
、 IFoo.Stub
等)在客户端和服务器之间共享(例如,这些类可以在引导类路径中)。共享类时,服务器也会链接到最新版本的类,即使它可能是使用旧版本的接口构建的。如果这个元接口在共享类中实现,它总是返回最新版本。然而,通过实现上述方法,接口的版本号被嵌入到服务器的代码中(因为IFoo.VERSION
是一个static final int
,在引用时被内联)因此该方法可以返回服务器构建的确切版本和。
处理旧接口
有可能客户端使用较新版本的 AIDL 接口进行了更新,但服务器使用的是旧的 AIDL 接口。在这种情况下,在旧接口上调用方法会返回UNKNOWN_TRANSACTION
。
有了稳定的 AIDL,客户就有了更多的控制权。在客户端,您可以为 AIDL 接口设置默认实现。只有在远程端未实现该方法时才会调用默认实现中的方法(因为它是使用较旧版本的接口构建的)。由于默认值是全局设置的,因此不应从潜在的共享上下文中使用它们。
Android 13 及更高版本中的 C++ 示例:
class MyDefault : public IFooDefault {
Status anAddedMethod(...) {
// do something default
}
};
// once per an interface in a process
IFoo::setDefaultImpl(::android::sp<MyDefault>::make());
foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
// remote side is not implementing it
Java 中的示例:
IFoo.Stub.setDefaultImpl(new IFoo.Default() {
@Override
public xxx anAddedMethod(...) throws RemoteException {
// do something default
}
}); // once per an interface in a process
foo.anAddedMethod(...);
您不需要在 AIDL 接口中提供所有方法的默认实现。保证在远程端实现的方法(因为您确定远程是在方法在 AIDL 接口描述中时构建的)不需要在默认的impl
类中被覆盖。
将现有的 AIDL 转换为结构化/稳定的 AIDL
如果您有现有的 AIDL 接口和使用它的代码,请使用以下步骤将该接口转换为稳定的 AIDL 接口。
确定接口的所有依赖项。对于接口所依赖的每个包,确定包是否在稳定的 AIDL 中定义。如果未定义,则必须转换包。
将界面中的所有 parcelables 转换为稳定的 parcelables(界面文件本身可以保持不变)。通过直接在 AIDL 文件中表达它们的结构来做到这一点。必须重写管理类才能使用这些新类型。这可以在您创建
aidl_interface
包之前完成(如下)。创建一个
aidl_interface
包(如上所述),其中包含您的模块名称、其依赖项以及您需要的任何其他信息。为了使其稳定(不仅仅是结构化),它还需要进行版本控制。有关详细信息,请参阅版本控制接口。