稳定的 AIDL

Android 10 增加了对稳定的 Android 接口定义语言 (AIDL) 的支持,这是一种跟踪 AIDL 接口提供的应用程序接口 (API)/应用程序二进制接口 (ABI) 的新方法。稳定版 AIDL 与 AIDL 具有以下主要区别:

  • 接口是在构建系统中使用aidl_interfaces定义的。
  • 接口只能包含结构化数据。表示所需类型的 Parcelable 会根据其 AIDL 定义自动创建,并自动编组和解组。
  • 接口可以声明为稳定的(向后兼容)。发生这种情况时,他们的 API 会在 AIDL 接口旁边的文件中进行跟踪和版本控制。

结构化 AIDL 与稳定 AIDL

结构化 AIDL是指纯粹在 AIDL 中定义的类型。例如,parcelable 声明(自定义 Parcelable)不是结构化 AIDL。其字段在 AIDL 中定义的 Parcelable 称为结构化 Parcelable

稳定的 AIDL需要结构化的 AIDL,以便构建系统和编译器可以了解对 Parcelable 所做的更改是否向后兼容。然而,并非所有结构化接口都是稳定的。为了保持稳定,接口必须仅使用结构化类型,并且还必须使用以下版本控制功能。相反,如果使用核心构建系统来构建接口或者设置了unstable:true接口不稳定。

定义 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 :用于打开或关闭跟踪的可选标志。从 Android 14 开始, cppjava后端默认为true
  • host_supported :可选标志,当设置为true时,生成的库可用于主机环境。
  • unstable :可选标志,用于标记该接口不需要稳定。当此设置为true时,构建系统既不会为接口创建 API 转储,也不需要更新它。
  • frozen :可选标志,当设置为true时,意味着该接口自上一个版本的接口以来没有发生任何更改。这可以实现更多的构建时检查。当设置为false时,这意味着接口正在开发中并且有新的更改,因此运行foo-freeze-api将生成新版本并自动将值更改为true 。在 Android 14 中引入。
  • 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.[cpp|ndk].additional_shared_libraries :在 Android 14 中引入,此标志添加了对本机库的依赖项。该标志对于ndk_headercpp_header很有用。
  • backend.java.sdk_version :可选标志,用于指定构建 Java 存根库所针对的 SDK 版本。默认值为"system_current" 。当backend.java.platform_apis为 true 时,不应设置此值。
  • backend.java.platform_apis :当生成的库需要针对平台 API 而不是 SDK 进行构建时,应将其设置为true可选标志。

对于版本和启用的后端的每个组合,都会创建一个存根库。如何引用特定后端的特定版本的存根库,请参见模块命名规则

编写 AIDL 文件

稳定 AIDL 中的接口与传统接口类似,不同之处在于它们不允许使用非结构化 Parcelable(因为它们不稳定!请参阅结构化与稳定 AIDL )。稳定 AIDL 的主要区别在于 Parcelable 的定义方式。此前,可分割物品是向前申报的;在稳定的(因此是结构化的)AIDL 中,parcelables 字段和变量是明确定义的。

// in a file like 'some/package/Thing.aidl'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}

目前支持(但不是必需) booleancharfloatdoublebyteintlongString的默认值。在 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-apiapi_diraidl_api/ name下添加一个新的 API 定义(具体取决于 Android 版本),并添加一个.hash文件,两者都代表接口的新冻结版本。 foo-freeze-api还会更新versions_with_info属性以反映附加版本和该版本的imports 。基本上, versions_with_info中的imports是从imports字段复制的。但是对于没有显式版本的导入,最新的稳定版本是在versions_with_infoimports中指定的。一旦指定了versions_with_info属性,构建系统就会在冻结版本之间以及树顶 (ToT) 和最新冻结版本之间运行兼容性检查。

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

为了保持界面的稳定性,所有者可以添加新的:

  • 接口末尾的方法(或具有显式定义的新序列的方法)
  • 位于 Parcelable 末尾的元素(需要为每个元素添加默认值)
  • 恒定值
  • 在 Android 11 中,枚举器
  • 在 Android 12 中,联合末尾的字段

不允许任何其他操作,并且任何其他人都不能修改界面(否则他们可能会与所有者所做的更改发生冲突)。

要测试所有接口是否都被冻结以供发布,您可以使用以下环境变量集进行构建:

  • AIDL_FROZEN_REL=true m ... - 构建要求冻结所有未指定owner:字段的稳定 AIDL 接口。
  • 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 未实现。

有关处理此问题的策略,请参阅查询版本使用默认值

包裹

当新字段添加到 Parcelable 时,旧客户端和服务器会删除它们。当新的客户端和服务器收到旧的 Parcelable 时,会自动填充新字段的默认值。这意味着需要为 Parcelable 中的所有新字段指定默认值。

客户端不应期望服务器使用新字段,除非他们知道服务器正在实现已定义该字段的版本(请参阅查询版本)。

枚举和常量

同样,客户端和服务器应该根据需要拒绝或忽略无法识别的常量值和枚举器,因为将来可能会添加更多。例如,服务器在收到它不知道的枚举器时不应中止。它应该忽略它,或者返回一些内容,以便客户端知道它在此实现中不受支持。

工会

如果接收者是旧的并且不知道该字段,则尝试发送具有新字段的联合会失败。该实现永远不会看到与新字段的联合。如果是单向事务,则忽略失败;否则错误为BAD_VALUE (对于 C++ 或 NDK 后端)或IllegalArgumentException (对于 Java 后端)。如果客户端将新字段的并集发送到旧服务器,或者旧客户端从新服务器接收并集,则会收到错误。

基于旗帜的发展

开发中(未冻结)的接口不能在发布设备上使用,因为它们不能保证向后兼容。

AIDL 支持这些未冻结接口库的运行时回退,以便针对最新未冻结版本编写代码并仍然在发布设备上使用。客户端的向后兼容行为与现有行为类似,并且通过回退,实现也需要遵循这些行为。请参阅使用版本化接口

AIDL 构建标志

控制此行为的标志是在build/release/build_flags.bzl中定义的RELEASE_AIDL_USE_UNFROZENtrue表示在运行时使用接口的未冻结版本, false表示未冻结版本的库的行为都与上次冻结版本相同。您可以将该标志覆盖为true以进行本地开发,但必须在发布之前将其恢复为false 。通常,开发是通过将标志设置为true配置来完成的。

兼容性矩阵和清单

供应商接口对象(VINTF 对象)定义所需的版本以及供应商接口两侧提供的版本。

大多数非 Cuttlefish 设备仅在接口冻结后才针对最新的兼容性矩阵,因此基于RELEASE_AIDL_USE_UNFROZEN的 AIDL 库没有差异。

合作伙伴拥有的接口被添加到设备在开发过程中针对的特定于设备或特定于产品的兼容性矩阵。因此,当接口的新的未冻结版本添加到兼容性矩阵时,需要保留以前的冻结版本以实现RELEASE_AIDL_USE_UNFROZEN=false 。您可以通过对不同的RELEASE_AIDL_USE_UNFROZEN配置使用不同的兼容性矩阵文件或允许在所有配置中使用的单个兼容性矩阵文件中允许两个版本来处理此问题。

例如,添加解冻版本 4 时,请使用<version>3-4</version>

当版本 4 被冻结时,您可以从兼容性矩阵中删除版本 3,因为当RELEASE_AIDL_USE_UNFROZENfalse时,将使用冻结的版本 4。

HAL 客户端更改

HAL 客户端代码必须向后兼容每个以前支持的冻结版本。当RELEASE_AIDL_USE_UNFROZENfalse时,服务始终看起来像最后一个冻结版本或更早的版本(例如,调用新的解冻方法返回UNKNOWN_TRANSACTION ,或者新的parcelable字段具有默认值)。 Android 框架客户端需要向后兼容其他以前的版本,但这对于供应商客户端和合作伙伴拥有的接口的客户端来说是一个新细节。

HAL 实施变更

HAL 开发与基于标志的开发的最大区别是要求 HAL 实现向后兼容最后的冻结版本,以便在RELEASE_AIDL_USE_UNFROZENfalse时工作。考虑实现和设备代码的向后兼容性是一项新的练习。请参阅使用版本化接口

对于客户端和服务器以及框架代码和供应商代码,向后兼容性注意事项通常是相同的,但您需要注意一些细微的差异,因为您现在正在有效地实现使用相同源代码的两个版本(当前的未冻结版本)。

示例:一个接口具有三个冻结版本。该界面已更新为新方法。客户端和服务都更新为使用新的版本 4 库。由于 V4 库基于接口的未冻结版本,因此当RELEASE_AIDL_USE_UNFROZENfalse时,它​​的行为类似于最后一个冻结版本(版本 3),并阻止使用新方法。

当接口被冻结时, RELEASE_AIDL_USE_UNFROZEN的所有值都使用该冻结版本,并且可以删除处理向后兼容性的代码。

在回调上调用方法时,必须妥善处理返回UNKNOWN_TRANSACTION情况。客户端可能会根据发布配置实现两个不同版本的回调,因此您不能假设客户端将发送最新版本,并且新方法可能会返回该版本。这类似于使用版本化接口中描述的稳定 AIDL 客户端如何保持与服务器的向后兼容性。

// Get the callback along with the version of the callback
ScopedAStatus RegisterMyCallback(const std::shared_ptr<IMyCallback>& cb) override {
    mMyCallback = cb;
    // Get the version of the callback for later when we call methods on it
    auto status = mMyCallback->getInterfaceVersion(&mMyCallbackVersion);
    return status;
}

// Example of using the callback later
void NotifyCallbackLater() {
  // From the latest frozen version (V2)
  mMyCallback->foo();
  // Call this method from the unfrozen V3 only if the callback is at least V3
  if (mMyCallbackVersion >= 3) {
    mMyCallback->bar();
  }
}

RELEASE_AIDL_USE_UNFROZENfalse并且服务尝试发送的新字段的值在退出进程时被丢弃时,现有类型( parcelableenumunion )中的新字段可能不存在或包含其默认值。

此未冻结版本中添加的新类型无法通过接口发送或接收。

RELEASE_AIDL_USE_UNFROZENfalse时,实现永远不会从任何客户端调用新方法。

请小心,仅将新的枚举器用于引入它们的版本,而不是以前的版本。

通常,您使用foo->getInterfaceVersion()来查看远程接口正在使用哪个版本。但是,通过基于标志的版本控制支持,您将实现两个不同的版本,因此您可能需要获取当前接口的版本。您可以通过获取当前对象的接口版本来完成此操作,例如this->getInterfaceVersion()my_ver的其他方法。有关详细信息,请参阅查询远程对象的接口版本

新的 VINTF 稳定接口

添加新的 AIDL 接口包时,没有最后冻结的版本,因此当RELEASE_AIDL_USE_UNFROZENfalse时,没有可回退的行为。不要使用这些接口。当RELEASE_AIDL_USE_UNFROZENfalse时,服务管理器将不允许服务注册接口,客户端也找不到它。

您可以根据设备 makefile 中RELEASE_AIDL_USE_UNFROZEN标志的值有条件地添加服务:

ifeq ($(RELEASE_AIDL_USE_UNFROZEN),true)
PRODUCT_PACKAGES += \
    android.hardware.health.storage-service
endif

如果服务是较大进程的一部分,因此您无法有条件地将其添加到设备,则可以检查该服务是否使用IServiceManager::isDeclared()进行声明。如果已声明但注册失败,则中止该过程。如果未声明,则预计将无法注册。

Cuttlefish 作为开发工具

每年,在 VINTF 冻结后,我们都会调整 Cuttlefish 的框架兼容性矩阵 (FCM) target-levelPRODUCT_SHIPPING_API_LEVEL ,以便它们反映明年发布的设备。我们调整target-levelPRODUCT_SHIPPING_API_LEVEL以确保有一些启动设备经过测试并满足明年发布的新要求。

RELEASE_AIDL_USE_UNFROZENtrue时,Cuttlefish 用于开发未来的 Android 版本。它针对明年的 Android 版本的 FCM 级别和PRODUCT_SHIPPING_API_LEVEL ,要求其满足下一版本的供应商软件要求 (VSR)。

RELEASE_AIDL_USE_UNFROZENfalse时,Cuttlefish 具有先前的target-levelPRODUCT_SHIPPING_API_LEVEL以反映发布设备。在 Android 14 及更低版本中,这种区分将通过不同的 Git 分支来实现,这些分支不会接收对 FCM target-level 、发布 API 级别或针对下一版本的任何其他代码的更改。

模块命名规则

在 Android 11 中,对于启用的版本和后端的每个组合,都会自动创建一个存根库模块。要引用特定的存根库模块进行链接,不要使用aidl_interface模块的名称,而是使用存根库模块的名称,即ifacename - version - backend ,其中

  • ifacenameaidl_interface模块的名称
  • version是以下任一版本
    • 冻结版本的V version-number
    • V latest-frozen-version-number + 1表示树尖(尚未冻结)版本
  • backend是以下任一者
    • java用于 Java 后端,
    • cpp用于 C++ 后端,
    • ndkndk_platform用于 NDK 后端。前者用于应用程序,后者用于平台使用,
    • 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)

与Android 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; }
}

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

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

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

  3. 创建一个aidl_interface包(如上所述),其中包含模块的名称、其依赖项以及您需要的任何其他信息。为了使其稳定(不仅仅是结构化),它还需要进行版本控制。有关更多信息,请参阅版本控制接口