Google 致力于为黑人社区推动种族平等。查看具体举措

稳定的 AIDL

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 R 开始,versions 冻结在 aidl_api/name 下。如果没有已冻结的接口版本,就不应指定此属性,且不会进行兼容性检查。
  • stability:可选标记,用于承诺此接口的稳定性。目前仅支持 "vintf"。如果未设置此属性,这意味着接口在此编译环境下才具有稳定性(因此,此处加载的接口只可与一起编译的元素共同使用,例如在 system.img 上)。如果将此标记设为 "vintf",这表示做出了稳定性承诺:只要有代码使用此接口,接口就必须保持稳定。
  • gen_trace:可选标记,用于开启或关闭跟踪功能。默认值为false
  • host_supported:可选标记,设置为 true 时,可将生成的库提供给主机环境。
  • unstable:可选标记,用于标记此接口不需要稳定。如果将此标记设置为 true,构建系统既不会为接口创建 API 转储,也不需要更新接口。
  • backend.<type>.enabled:这些标记用于切换后端的启用状态,AIDL 编译器会为这些后端生成代码。目前支持 3 个后端:javacppndk。默认情况下,所有后端都处于启用状态。如果不需要特定的后端,则需要明确停用此后端。
  • 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;
}

现在支持 booleancharfloatdoublebyteintlongString 的默认值(但不是必需的)。

使用桩库

将桩库作为依赖项添加到模块之后,您可以将这些库添加到您的文件中。下面是构建系统中的桩库的示例(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_diraidl_api/name 下(因 Android 版本而异)为接口的下一个版本添加新的 API 定义。构建此目标还会更新 versions 属性,以反映其他版本。指定 versions 属性之后,构建系统会在冻结版本之间以及主干顶部 (ToT) 与最新冻结版本之间运行兼容性检查。

此外,您还需要管理 ToT 版本的 API 定义。每当 API 更新时,运行 foo-update-api,以更新包含 ToT 版本的 API 定义的 aidl_api/name/current

为了保持接口的稳定性,所有者可以:

  • 在接口的末尾添加新方法(或添加具有显式定义的新序列的方法)
  • 在 Parcel 的末尾添加新元素(需要为每个元素添加一个默认值)
  • 添加新常量值
  • 在 Android R 中,添加新枚举器

不允许进行其他任何操作,其他任何人都不能修改接口,否则可能会与所有者进行的更改发生冲突。

在运行时,如果尝试在旧服务器上调用新方法,新客户端便会自动收到 UNKNOWN_TRANSACTION。如需了解如何处理这种情况,请参阅查询版本使用默认值。如果将新字段添加到 Parcelable,旧客户端/服务器会删除这些字段。如果新客户端/服务器收到旧 Parcelable,系统会自动填入新字段的默认值。也就是说,您应该为 Parcelable 中的所有新字段指定默认值。同样,客户端/服务器应根据需要拒绝或忽略无法识别的常量值和枚举器,因为将来可能会进一步添加。

模块命名规则

在 Android R 中,对于每个版本与已启用后端的组合,系统都会自动创建一个桩库模块。如需引用特定的桩库模块以用于链接,请不要使用 aidl_interface 模块的名称,而要使用桩库模块的名称,即 ifacename-version-backend,其中

  • ifacenameaidl_interface 模块的名称
  • version 为以下值之一:
    • Vversion-number,适用于已冻结版本
    • unstable,适用于最新(尚未冻结)版本
  • backend 为以下值之一:
    • java,适用于 Java 后端;
    • cpp,适用于 C++ 后端;
    • ndkndk_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; }
}

这是因为生成的类(IFooIFoo.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 接口。

  1. 确定您的接口的所有依赖项。对于该接口所依赖的每个软件包,确定该软件包是否已在稳定的 AIDL 中定义。如果未定义,则必须转换该软件包。

  2. 将您的接口中的所有 Parcelable 全部转换为稳定的 Parcelable(接口文件本身可以保持不变)。可通过直接在 AIDL 文件中表示它们的结构来实现这一点。您必须重写管理类以使用这些新类型。可以在创建 aidl_interface 软件包(如下所示)之前完成重写。

  3. 创建 aidl_interface 软件包(如上所述),其中应包含模块的名称和依赖项以及您需要的任何其他信息。为使其保持稳定(不仅仅是结构化),还需要对其进行版本编号。如需了解详情,请参阅对接口进行版本编号