稳定的 AIDL

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

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来更新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 ,其中

  • ifacenameaidl_interface模块的名称
  • version
    • V version-number
    • V latest-frozen-version-number + 1树尖(尚未冻结)版本
  • backend
    • java用于 Java 后端,
    • 用于 C++ 后端的cpp
    • NDK 后端的ndkndk_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; }
}

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

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