Android 10 添加了对稳定的 Android 接口定义语言 (AIDL) 的支持,这是一种跟踪由 AIDL 接口提供的应用编程接口 (API)/应用二进制接口 (ABI) 的新方法。稳定的 AIDL 与 AIDL 的主要区别如下:
- 在构建系统中使用
aidl_interfaces
定义接口。 - 接口只能包含结构化数据。对于代表所需类型的 Parcelable,系统会根据其 AIDL 定义自动创建,并自动对其进行编组和解组。
- 可以将接口声明为“稳定”接口(向后兼容)。声明之后,会在 AIDL 接口旁的一个文件中对这些接口的 API 进行跟踪和版本编号。
定义 AIDL 接口
aidl_interface
的定义如下:
aidl_interface {
name: "my-aidl",
srcs: ["srcs/aidl/**/*.aidl"],
local_include_dir: "srcs/aidl",
imports: ["other-aidl"],
versions: ["1", "2"],
stability: "vintf",
backend: {
java: {
enabled: true,
platform_apis: true,
},
cpp: {
enabled: true,
},
ndk: {
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 接口模块的可选列表。可通过import
语句访问导入的 AIDL 接口中定义的 AIDL 类型。versions
:冻结在api_dir
下的接口的先前版本。从 Android 11 开始,versions
冻结在aidl_api/name
下。 如果没有已冻结的接口版本,就不应指定此属性,且不会进行兼容性检查。stability
:可选标记,用于承诺此接口的稳定性。目前仅支持"vintf"
。如果未设置此属性,这意味着接口在此编译环境下才具有稳定性(因此,此处加载的接口只可与一起编译的元素共同使用,例如在 system.img 上)。如果将此标记设为"vintf"
,这表示做出了稳定性承诺:只要有代码使用此接口,接口就必须保持稳定。gen_trace
:可选标记,用于开启或关闭跟踪功能。默认值为false
。host_supported
:可选标记,设置为true
时,可将生成的库提供给主机环境。unstable
:可选标记,用于标记此接口不需要稳定。如果将此标记设置为true
,构建系统既不会为接口创建 API 转储,也不需要更新接口。backend.<type>.enabled
:这些标记用于切换后端的启用状态,AIDL 编译器会为这些后端生成代码。目前支持 3 个后端:java
、cpp
和ndk
。默认情况下,所有后端都处于启用状态。如果不需要特定的后端,则需要明确停用此后端。backend.<type>.apex_available
:生成的桩库支持的 APEX 名称的列表。backend.[cpp|java].gen_log
:可选标记,用于控制是否生成其他代码以收集有关事务的信息。backend.[cpp|java].vndk.enabled
:可选标记,用于将此接口变为 VNDK 的一部分。默认值为false
。backend.java.platform_apis
:可选标记,用于控制是否根据平台中的私有 API 构建 Java 桩库。如果将stability
设为"vintf"
,此标记应设为"true"
。backend.java.sdk_version
:可选标记,用于指定构建 Java 桩库所基于的 SDK 版本。默认设置为"system_current"
。如果backend.java.platform_apis
为 true,就不应设置此标记。backend.java.platform_apis
:可选标记,当生成的库需要针对平台 API 而非 SDK 进行构建时,应将此标记设置为true
。
对于 versions
与已启用后端的每个组合,系统都会创建一个桩库。如需了解如何针对特定的后端引用特定版本的桩库,请参阅模块命名规则。
编写 AIDL 文件
稳定 AIDL 中的接口与传统接口相似,不同之处在于前者不允许使用非结构化的 Parcelable,因为这些 Parcelable 不稳定。稳定 AIDL 的最大不同就在于如何定义 Parcelable。以前,Parcelable 是前向声明的,而在稳定的 AIDL 中,Parcelable 字段和变量是显式定义的。
// 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.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"],
...
}
采用 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
对接口进行版本编号
如果声明一个名为 foo 的模块,则同时也会在构建系统中创建一个目标,您可以用该目标管理该模块的 API。构建后,foo-freeze-api 会在 api_dir
或 aidl_api/name
下(因 Android 版本而异)为接口的下一个版本添加新的 API 定义。构建此目标还会更新 versions
属性,以反映其他版本。指定 versions
属性之后,构建系统会在冻结版本之间以及主干顶部 (ToT) 与最新冻结版本之间运行兼容性检查。
此外,您还需要管理 ToT 版本的 API 定义。每当 API 更新时,运行 foo-update-api,以更新包含 ToT 版本的 API 定义的 aidl_api/name/current
。
为了保持接口的稳定性,所有者可以:
- 在接口的末尾添加新方法(或添加具有显式定义的新序列的方法)
- 在 Parcel 的末尾添加新元素(需要为每个元素添加一个默认值)
- 添加新常量值
- 在 Android 11 中,添加新枚举器
不允许进行其他任何操作,其他任何人都不能修改接口,否则可能会与所有者进行的更改发生冲突。
在运行时,如果尝试在旧服务器上调用新方法,新客户端便会自动收到 UNKNOWN_TRANSACTION
。如需了解如何处理这种情况,请参阅查询版本和使用默认值。如果将新字段添加到 Parcelable,旧客户端/服务器会删除这些字段。如果新客户端/服务器收到旧 Parcelable,系统会自动填入新字段的默认值。也就是说,您应该为 Parcelable 中的所有新字段指定默认值。同样,客户端/服务器应根据需要拒绝或忽略无法识别的常量值和枚举器,因为将来可能会进一步添加。
模块命名规则
在 Android 11 中,对于每个版本与已启用后端的组合,系统都会自动创建一个桩库模块。如需引用特定的桩库模块以用于链接,请不要使用 aidl_interface
模块的名称,而要使用桩库模块的名称,即 ifacename-version-backend,其中
ifacename
:aidl_interface
模块的名称version
为以下值之一:Vversion-number
,适用于已冻结版本unstable
,适用于最新(尚未冻结)版本
backend
为以下值之一:java
,适用于 Java 后端;cpp
,适用于 C++ 后端;ndk
或ndk_platform
,适用于 NDK 后端。前者适用于应用,后者适用于平台使用。
对于最新的已冻结版本,省略除 Java 目标之外的版本字段。也就是说,系统不会生成 module-Vlatest-frozen-version-(cpp|ndk|ndk_platform)
。
假设有一个名为 foo 的模块,其最新版本为 2,且同时支持 NDK 和 C++。在这种情况下,AIDL 会生成以下模块:
- 基于版本 1
foo-V1-(java|cpp|ndk|ndk_platform)
- 基于版本 2(最新的稳定版本)
foo-(java|cpp|ndk|ndk_platform)
foo-V2-java
(与 foo-java 内容相同)
- 基于 ToT 版本
foo-unstable-(java|cpp|ndk|ndk_platform)
在大多数情况下,输出文件名与模块名称相同。但是,对于 ToT 版本的 C++ 或 NDK 模块,输出文件名与模块名称不同。
例如,foo-unstable-cpp
的输出文件名为 foo-V3-cpp.so
,而不是 foo-unstable-cpp.so
,如下所示。
- 基于版本 1:
foo-V1-(cpp|ndk|ndk_platform).so
- 基于版本 2:
foo-V2-(cpp|ndk|ndk_platform).so
- 基于 ToT 版本:
foo-V3-(cpp|ndk|ndk_platform).so
新增的元接口方法
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
}
使用 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
}
使用 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
}
对于 Java 语言,远程端必须以如下方式实现 getInterfaceVersion()
:
class MyFoo extends IFoo.Stub {
@Override
public final int getInterfaceVersion() { return IFoo.VERSION; }
}
这是因为生成的类(IFoo
、IFoo.Stub
等)将在客户端和服务器之间共享(例如,这些类可以位于启动类路径下)。共享类时,服务器仍会链接到类的最新版本,即使该版本可能是用接口的旧版本构建的。如果该元接口是在共享类中实现的,该接口始终会返回最新版本。不过,如果按照上述方式实现该方法,便会将接口的版本号嵌入到服务器的代码中(因为 IFoo.VERSION
是一个在引用时内嵌的 static final int
),从而使该方法能够返回构建该服务器时所用的确切版本。
处理旧版接口
可能会存在以下情况:客户端更新了较新的 AIDL 接口版本,而服务器使用的是旧版 AIDL 接口。在这种情况下,在旧接口上调用方法会返回 UNKNOWN_TRANSACTION
。
使用稳定的 AIDL,客户端将拥有更多的控制权。在客户端,您可以设置 AIDL 接口的默认实现。对于默认实现中的方法,只有当其没有在远程端实现时,才会被调用。这是因为此方法是使用旧版接口构建的。由于默认值是全局设置的,因此不应该在可能会共享的上下文中使用。
采用 C++ 语言的示例:
class MyDefault : public IFooDefault {
Status anAddedMethod(...) {
// do something default
}
};
// once per an interface in a process
IFoo::setDefaultImpl(std::unique_ptr<IFoo>(MyDefault));
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 接口中的所有方法提供默认实现。您也不需要在默认 impl
类中替换一定会在远程端实现的方法(因为您确定远程端是在这些方法位于 AIDL 接口描述中时构建的)。
将现有的 AIDL 转换为结构化/稳定的 AIDL
如果您已经有一个 AIDL 接口以及使用该接口的代码,可以按照以下步骤将该接口转换为稳定的 AIDL 接口。
确定您的接口的所有依赖项。对于该接口所依赖的每个软件包,确定该软件包是否已在稳定的 AIDL 中定义。如果未定义,则必须转换该软件包。
将您的接口中的所有 Parcelable 全部转换为稳定的 Parcelable(接口文件本身可以保持不变)。可通过直接在 AIDL 文件中表示它们的结构来实现这一点。您必须重写管理类以使用这些新类型。可以在创建
aidl_interface
软件包(如下所示)之前完成重写。创建
aidl_interface
软件包(如上所述),其中应包含模块的名称和依赖项以及您需要的任何其他信息。为使其保持稳定(不仅仅是结构化),还需要对其进行版本编号。如需了解详情,请参阅对接口进行版本编号。