使用新版 API

本页将介绍您的应用在新操作系统上运行时该如何使用新的操作系统功能。 同时保持与旧设备的兼容性。

默认情况下,应用中对 NDK API 的引用属于强引用。 如果您的库 。如果未找到这些符号,应用将中止。这与 Java 的行为方式,在缺失的 API 被 调用。

因此,NDK 会阻止您创建对 比应用的 minSdkVersion 更新的 API。这可防止您 意外地传送了在测试期间有效但未能加载的代码 对于较旧的版本,(UnsatisfiedLinkError 将从 System.loadLibrary() 抛出) 设备。另一方面,使用 API 编写代码更加困难 比应用的 minSdkVersion 更新,因为您必须使用 dlopen()dlsym(),而不是普通的函数调用。

强引用的替代方案是使用弱引用。较弱 导致系统生成 该符号应设置为 nullptr,而不是无法加载。他们仍然 不能安全地调用,但只要调用点受到保护以阻止调用即可 API 不可用时,可以运行其余代码,并且您可以 无需使用 dlopen()dlsym(),即可正常调用 API。

弱 API 引用不需要动态链接器的额外支持, 因此适用于任何 Android 版本

在 build 中启用弱 API 引用

CMake

运行 CMake 时传递 -DANDROID_WEAK_API_DEFS=ON。如果您通过 externalNativeBuild,请将以下内容添加到您的 build.gradle.kts(或 如果您仍在使用 build.gradle,则等效于 Groovy):

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-build

请将以下内容添加到 Application.mk 文件:

APP_WEAK_API_DEFS := true

如果您还没有 Application.mk 文件,请在同一位置创建一个 目录中指定为 Android.mk 文件。对 ndk-build 不需要 build.gradle.kts(或 build.gradle)文件。

其他构建系统

如果您未使用 CMake 或 ndk-build,请查阅 build 的相关文档 看看是否有推荐方法可以启用此功能。如果您的 build 系统本身不支持此选项,您可以 在编译时传递以下标志:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

第一种方法用于配置 NDK 标头,以允许弱引用。第二个回合 将有关不安全 API 调用的警告转换为错误。

如需了解详情,请参阅构建系统维护人员指南

受保护的 API 调用

此功能并不能神奇地确保对新 API 的调用是安全的。我唯一能做的就是 将加载时间错误推迟到调用时错误。好处是 都可以在运行时保护该调用并妥善回退,无论是使用 或者通知用户相应应用的相应功能 或完全避开该代码路径。

如果您发出无保护机制,Clang 可以发出警告 (unguarded-availability) 调用的 API 不适用于应用的 minSdkVersion。如果您 使用 ndk-build 或我们的 CMake 工具链文件,该警告会自动 并升级成了错误。

以下示例代码展示了如何有条件地使用某个 API, 此功能已启用(使用 dlopen()dlsym()):

void LogImageDecoderResult(int result) {
    void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
    CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
    auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
        dlsym(lib, "AImageDecoder_resultToString")
    );
    if (func == nullptr) {
        LOG(INFO) << "cannot stringify result: " << result;
    } else {
        LOG(INFO) << func(result);
    }
}

读起来有点复杂,因为这里有一些重复的函数名称(如果 您同时编写了 C 和签名),它可以成功构建,但始终 如果您不小心输错传递的函数名称,则会在运行时进行回退 dlsym,并且必须对每个 API 使用此模式。

如果 API 引用较弱,则上述函数可重写为:

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

在后台,__builtin_available(android 31, *) 会调用 android_get_device_api_level(),缓存结果,并将其与 31 进行比较 (这是引入了 AImageDecoder_resultToString() 的 API 级别)。

要确定对 __builtin_available 使用哪个值,最简单的方法是 有人试图在没有守卫者的情况下建造 __builtin_available(android 1, *))并按照错误消息中的说明操作。 例如,使用AImageDecoder_createFromAAsset() minSdkVersion 24 将生成:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

在这种情况下,调用应由 __builtin_available(android 30, *) 保护。 如果没有构建错误, minSdkVersion 且无需防护,或者 build 配置错误, “unguarded-availability”警告已停用。

或者,NDK API 引用也会显示如下内容: “在 API 30 中引入”。如果这些文字不存在,则意味着: 该 API 适用于所有受支持的 API 级别。

避免重复使用 API Guard

如果您使用此方法,则应用中可能会有一些代码段 只能在足够新的设备上使用。与其重复 __builtin_available() 签入您的每个函数,您就可以为自己的 因为需要特定的 API 级别。例如,ImageDecoder API 它们本身都是在 API 30 中添加的,因此,对于大量使用上述属性的函数, API,您可以执行以下操作:

#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)

void DecodeImageWithImageDecoder() REQUIRES_API(30) {
    // Call any APIs that were introduced in API 30 or newer without guards.
}

void DecodeImageFallback() {
    // Pay the overhead to call the Java APIs via JNI, or use third-party image
    // decoding libraries.
}

void DecodeImage() {
    if (API_AT_LEAST(30)) {
        DecodeImageWithImageDecoder();
    } else {
        DecodeImageFallback();
    }
}

API Guard 的奇怪

Clang 对 __builtin_available 的使用方式非常讲究。只有字面量 (尽管可能已进行了宏替换)if (__builtin_available(...)) 可正常运行。均匀 if (!__builtin_available(...)) 之类的简单操作不起作用(Clang) 将发出 unsupported-availability-guard 警告 unguarded-availability)。这可能会在未来版本的 Clang 中得到改进。请参阅 LLVM 问题 33161 了解更多信息。

unguarded-availability 的检查仅会应用于它们所在的函数作用域 。即使进行 API 调用的函数 只从受保护的范围内调用。为了避免在 自己的代码,请参阅避免重复 API 防护

为什么它不是默认选项?

除非使用得当,否则高 API 引用与弱 API 之间的区别 显而易见,前者会快速失败,而 在用户采取导致 API 缺失的操作之前,后者不会失败 调用该方法。在这种情况下,错误消息不会清晰显示 编译时“AFoo_bar() is not available”则属于分段错误。包含 强引用,错误消息更清晰,而快速失败则是 更安全的默认设置

由于这是一项新功能,因此很少编写现有代码来处理 这一行为第三方代码不是针对 Android 编写的 可能一直都会存在这个问题,因此,目前没有针对 会不断变化的默认行为

我们确实推荐您使用此功能,但这种做法会造成更多问题 检测和调试风险,但是您在知情的情况下 比起在您不知情的情况下行为就会发生变化

注意事项

此功能适用于大多数 API,但在少数情况下并不适用 工作。

较不可能出现问题的是较新的 libc API。与 Android API,这些 API 通过标头中的 #if __ANDROID_API__ >= X 进行保护 而不仅仅是 __INTRODUCED_IN(X),这样甚至可以防止弱声明 被用户看到由于对现代 NDK 支持最早的 API 级别是 r21, 已有常用的 libc API。每个添加了新的 libc API 版本(请参阅status.md),但版本越新,就越有可能 是很少有开发者需要的极端情况。也就是说,如果您是 这些开发者,目前您需要继续使用 dlsym() 来调用它们 如果您的 minSdkVersion 早于 API 版本,则会创建 API。这是一个可以解决的问题, 但这样做会破坏所有应用(任何应用 包含 libc API 的 polyfill 的代码将无法编译,因为 libc 和本地声明上的 availability 属性不匹配,因此 我们不确定能否修复该问题,或者何时修复。

更多开发者可能会遇到以下情况: 包含的新 API 比您的 minSdkVersion 更新。仅限此功能 启用弱符号引用;不存在安全系数低的库 参考。例如,如果您的minSdkVersion是 24 岁,您可以关联 libvulkan.so,并对 vkBindBufferMemory2 进行受保护的调用,因为 libvulkan.so 在搭载 API 24 及以上级别的设备上使用。另一方面 如果您的 minSdkVersion 为 23,则必须回退到 dlopendlsym 因为当设备只支持 API 23。我们不知道一个好的解决方案可以解决这个问题, 这个词,它会自行解决,因为我们(如果可能)不再允许新的 用于创建新库的 API。

面向图书馆作者

如果您要开发用于 Android 应用的库,您应该: 请避免在公开标头中使用此功能。它可以在 但如果您在自己的源代码文件的任何代码中都依赖 __builtin_available, 标头,例如内联函数或模板定义,您可以强制所有 启用此功能出于同样的原因 功能,则应避免代表开发者做出该选择, 您的消费者。

如果您确实需要在公开标头中实现此行为,请确保 以便用户知道他们需要启用该功能,并且 也应了解这么做的风险