稳定的 AIDL

Android 10 添加了对稳定的 Android 接口定义语言 (AIDL) 的支持,这是一种跟踪由 AIDL 接口提供的应用编程接口 (API) 和应用二进制接口 (ABI) 的新方法。稳定 AIDL 的运作方式与 AIDL 完全相同,但构建系统会跟踪接口兼容性,并且会对您可以执行的操作施加限制:

  • 在构建系统中使用 aidl_interfaces 定义接口。
  • 接口只能包含结构化数据。对于代表所需类型的 Parcelable,系统会根据其 AIDL 定义自动创建,并自动对其进行编组和解组。
  • 可以将接口声明为“稳定”接口(向后兼容)。声明之后,会在 AIDL 接口旁的一个文件中对这些接口的 API 进行跟踪和版本编号。

结构化 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 下。如果没有已冻结的接口版本,就不应指定此属性,且不会进行兼容性检查。对于 Android 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"。如果未设置 stability,除非指定 unstable,否则构建系统会检查接口是否向后兼容。如果未设置此属性,则意味着接口在此编译环境(所有系统元素,例如 system.img 和相关分区中的元素,或所有供应商元素,例如 vendor.img 和相关分区中的元素)下具有稳定性。如果将 stability 设为 "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 默认处于停用状态,直到 Android 15。
  • 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_apistrue,就不应设置此标志。
  • backend.java.platform_apis:可选标志,当生成的库需要针对平台 API 而非 SDK 进行构建时,应将此标志设置为 true

对于版本与已启用后端的每个组合,系统都会创建一个桩库。如需了解如何针对特定的后端引用特定版本的桩库,请参阅模块命名规则

编写 AIDL 文件

稳定 AIDL 中的接口与传统接口相似,不同之处在于前者不允许使用非结构化的 Parcelable(因为这些 Parcelable 不稳定!请参阅结构化 AIDL 与稳定的 AIDL)。稳定 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 12 中,还支持用户定义的枚举的默认值。如果未指定默认值,系统会使用类似 0 的值或空值。即使没有零枚举器,没有默认值的枚举也会初始化为 0。

使用桩库

将桩库作为依赖项添加到模块之后,您可以将这些库添加到您的文件中。下面是构建系统中的桩库的示例(Android.mk 也可用于旧版模块定义):

cc_... {
    name: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // can also be shared_libs if your preference 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: ...,
    rustlibs: ["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_diraidl_api/name 下(因 Android 版本而异)添加新的 API 定义,并添加一个 .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 ... - build 需要冻结所有稳定的 AIDL 接口,这些接口未指定 owner: 字段。
  • AIDL_FROZEN_OWNERS="aosp test" - build 需要冻结所有稳定的 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

如果将新字段添加到 Parcelable,旧客户端和服务器会删除这些字段。如果新客户端和服务器收到旧 Parcelable,系统会自动填入新字段的默认值。这意味着,您需要为 Parcelable 中的所有新字段指定默认值。

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

枚举和常量

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

联合体

如果接收方是旧的并且不了解某个新字段,尝试发送包含该字段的联合体会失败。此实现绝不会看到包含新字段的联合体。如果是单向事务,系统会忽略失败情况;否则,错误为 BAD_VALUE(对于 C++ 或 NDK 后端)或 IllegalArgumentException(对于 Java 后端)。如果客户端在向旧服务器发送设为新字段的联合体,或者是从新服务器接收联合体的旧客户端,则会收到此错误。

管理多个版本

Android 中的链接器命名空间只能包含特定 aidl 接口的 1 个版本,以免生成的 aidl 类型具有多个定义。C++ 具有单一定义规则,该规则要求每个符号只有一个定义。

如果某个模块依赖于同一 aidl_interface 库的不同版本,Android build 会显示错误。该模块可能直接或间接(通过其依赖项的依赖项)依赖于这些库。这些错误会显示从失败的模块到 aidl_interface 库的冲突版本的依赖关系图。所有这些依赖项都需要更新,以包含这些库的相同(通常是最新)版本。

如果许多不同的模块使用接口库,那么为需要使用同一版本的任何一组库和进程创建 cc_defaultsjava_defaultsrust_defaults 会很有帮助。引入新版接口时,这些默认值可以更新,并且使用这些默认值的所有模块也会一同更新,确保它们不会使用不同版本的接口。

cc_defaults {
  name: "my.aidl.my-process-group-ndk-shared",
  shared_libs: ["my.aidl-V3-ndk"],
  ...
}

cc_library {
  name: "foo",
  defaults: ["my.aidl.my-process-group-ndk-shared"],
  ...
}

cc_binary {
  name: "bar",
  defaults: ["my.aidl.my-process-group-ndk-shared"],
  ...
}

aidl_interface 模块导入其他 aidl_interface 模块时,这会创建其他依赖项,这些依赖项需要特定版本才能一起使用。如果在同一进程中一起使用的多个 aidl_interface 模块中导入了通用的 aidl_interface 模块,这种情况可能会变得难以管理。

aidl_interfaces_defaults 可用于保留 aidl_interface 的最新版本依赖项的单一定义,该接口可在一个位置进行更新,并供想要导入该通用接口的所有 aidl_interface 模块使用。

aidl_interface_defaults {
  name: "android.popular.common-latest-defaults",
  imports: ["android.popular.common-V3"],
  ...
}

aidl_interface {
  name: "android.foo",
  defaults: ["my.aidl.latest-ndk-shared"],
  ...
}

aidl_interface {
  name: "android.bar",
  defaults: ["my.aidl.latest-ndk-shared"],
  ...
}

基于标志的开发

处于开发阶段(未冻结)的接口无法在已发布设备上使用,因为它们不能保证具有向后兼容性。

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

AIDL build 标志

build/release/build_flags.bzl 中定义的 RELEASE_AIDL_USE_UNFROZEN 标志用于控制此行为。true 表示在运行时使用的是接口的未冻结版本,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。

清单

在 Android 15 中,引入了 libvintf 的更改,以便根据 RELEASE_AIDL_USE_UNFROZEN 的值在构建时修改清单文件。

清单和清单 fragment 会声明服务实现的接口版本。使用接口的最新未冻结版本时,必须更新清单以反映此新版本。RELEASE_AIDL_USE_UNFROZEN=false 时,清单条目会由 libvintf 进行调整,以反映生成的 AIDL 库中的更改。版本会从未冻结的版本 N 修改为上一个冻结的版本 N - 1。因此,用户无需为每项服务管理多个清单或清单 fragment。

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,Service Manager 将不允许服务注册接口,客户端也找不到接口。

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

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

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

将 Cuttlefish 用作开发工具

VINTF 被冻结后,每年我们都会调整框架兼容性矩阵 (FCM) target-level 和 Cuttlefish 的 PRODUCT_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 为以下值之一:
    • Vversion-number,适用于已冻结版本
    • Vlatest-frozen-version-number + 1,适用于最新(尚未冻结)版本
  • backend 为以下值之一:
    • java,适用于 Java 后端;
    • cpp,适用于 C++ 后端;
    • ndkndk_platform,适用于 NDK 后端。前者适用于应用,后者适用于平台使用(直到 Android 13)。在 Android 13 及更高版本中,请仅使用 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,以避免复制和粘贴错误)。根据 javac 配置,可能需要使用注解 @SuppressWarnings("static") 来停用警告:

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 接口中的所有方法提供默认实现。您也不需要在默认 impl 类中替换一定会在远程端实现的方法(因为您确定远程端是在这些方法位于 AIDL 接口描述中时构建的)。

将现有的 AIDL 转换为结构化或稳定的 AIDL

如果您已经有一个 AIDL 接口以及使用该接口的代码,可以按照以下步骤将该接口转换为稳定的 AIDL 接口。

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

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

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