使用较新的 API

此页面说明当您的应用在新的操作系统版本上运行时,如何使用新的操作系统功能,同时保持与旧设备的兼容性。

默认情况下,应用程序中对 NDK API 的引用是强引用。Android 的动态加载器将在加载库时急切地解析它们。如果找不到符号,应用程序将中止。这与 Java 的行为相反,在 Java 中,直到调用缺少的 API 时才会抛出异常。

因此,NDK 将阻止您创建比应用的minSdkVersion更新的 API 的强引用。这可以防止您意外地发布在测试期间有效的代码,但在旧设备上加载失败(UnsatisfiedLinkError将从System.loadLibrary()抛出)。另一方面,编写使用比应用程序的minSdkVersion更新的 API 的代码更加困难,因为您必须使用dlopen()dlsym()调用 API,而不是使用正常的函数调用。

使用弱引用是使用强引用的替代方案。在加载库时找不到的弱引用会导致该符号的地址设置为nullptr,而不是加载失败。它们仍然不能安全地被调用,但只要调用站点被保护以防止在不可用时调用 API,其余代码就可以运行,并且您可以正常调用 API,而无需使用dlopen()dlsym()

弱 API 引用不需要动态链接器的额外支持,因此它们可以与任何版本的 Android 一起使用。

在您的构建中启用弱 API 引用

CMake

运行 CMake 时传递-DANDROID_WEAK_API_DEFS=ON。如果您通过externalNativeBuild使用 CMake,请将以下内容添加到您的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,请查阅构建系统的文档,了解是否有推荐的方法来启用此功能。如果您的构建系统本身不支持此选项,则可以在编译时传递以下标志来启用此功能

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

第一个配置 NDK 头文件以允许弱引用。第二个将不安全 API 调用的警告转换为错误。

有关更多信息,请参阅构建系统维护者指南

受保护的 API 调用

此功能不会神奇地使对新 API 的调用变得安全。它唯一做的事情是将加载时错误延迟到调用时错误。好处是您可以在运行时保护该调用并优雅地回退,无论是使用备用实现还是通知用户该应用的功能在他们的设备上不可用,或者完全避免该代码路径。

当您对应用的minSdkVersion不可用的 API 进行无保护调用时,Clang 可以发出警告(unguarded-availability)。如果您使用 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, *)的保护)的情况下构建,并执行错误消息告诉您的操作。例如,对minSdkVersion 24AImageDecoder_createFromAAsset()的无保护调用将产生

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

在这种情况下,调用应受__builtin_available(android 30, *)保护。如果没有构建错误,则 API 始终可用于您的minSdkVersion,并且不需要保护,或者您的构建配置错误,并且unguarded-availability警告被禁用。

或者,NDK API 参考将在每个 API 中说明“在 API 30 中引入”。如果该文本不存在,则表示该 API 可用于所有受支持的 API 级别。

避免 API 保护的重复

如果您使用此功能,您的应用中可能会有某些代码段只能在新设备上使用。与其在每个函数中重复__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 保护的特性

Clang 对__builtin_available的使用方式非常严格。只有字面量(可能是宏替换的)if (__builtin_available(...))有效。即使是像if (!__builtin_available(...))这样的琐碎操作也不起作用(Clang 将发出unsupported-availability-guard警告以及unguarded-availability)。这可能会在未来版本的 Clang 中得到改进。有关更多信息,请参阅LLVM Issue 33161

unguarded-availability的检查仅适用于使用它们的函数范围。即使带有 API 调用的函数仅从受保护的范围内调用,Clang 也会发出警告。要避免在您自己的代码中重复保护,请参阅避免 API 保护的重复

为什么这不是默认设置?

除非正确使用,强 API 引用和弱 API 引用之间的区别在于,前者将快速且明显地失败,而后者将不会失败,直到用户采取导致缺少的 API 被调用的操作。当这种情况发生时,错误消息将不是清晰的编译时“AFoo_bar()不可用”错误,而是一个段错误。使用强引用,错误消息更加清晰,并且快速失败是更安全的默认设置。

由于这是一个新功能,因此很少有现有代码被编写成安全地处理此行为。非面向 Android 编写的第三方代码很可能会始终存在此问题,因此目前没有计划更改默认行为。

我们建议您使用此功能,但由于它会使问题更难以检测和调试,因此您应该明知其风险而接受这些风险,而不是在您不知情的情况下行为发生变化。

注意事项

此功能适用于大多数 API,但有一些情况它不起作用。

最不可能出现问题的是较新的 libc API。与其他 Android API 不同,这些 API 在头文件中使用#if __ANDROID_API__ >= X而不是__INTRODUCED_IN(X)进行保护,这可以防止甚至看到弱声明。由于最旧的 API 级别现代 NDK 支持的是 r21,因此最常用的 libc API 已经可用。每个版本都会添加新的 libc API(请参阅status.md),但它们越新,越有可能成为很少有开发者需要的边缘情况。也就是说,如果您是其中一位开发者,那么目前您需要继续使用dlsym()来调用这些 API,如果您的minSdkVersion早于该 API。这是一个可解决的问题,但这样做会带来破坏所有应用源代码兼容性的风险(任何包含polyfills的 libc API 的代码都将无法编译,因为 libc 和本地声明上的availability属性不匹配),因此我们不确定是否或何时会修复它。

开发人员最常遇到的情况是,包含新 API 的版本比您的 minSdkVersion 新。此功能仅启用弱符号引用;不存在弱库引用这种东西。例如,如果您的 minSdkVersion 为 24,则可以链接 libvulkan.so 并对 vkBindBufferMemory2 进行受保护的调用,因为 libvulkan.so 可用于 API 24 及更高版本的设备。另一方面,如果您的 minSdkVersion 为 23,则必须回退到 dlopendlsym,因为该库在仅支持 API 23 的设备上将不存在。我们不知道解决此问题的有效方法,但从长远来看,它会自行解决,因为我们(在可能的情况下)不再允许新的 API 创建新的库。

面向库作者

如果您正在开发要在 Android 应用程序中使用的库,则应避免在公共头文件中使用此功能。它可以在内联代码之外安全地使用,但如果您在头文件中的任何代码(例如内联函数或模板定义)中依赖于 __builtin_available,则会强制所有使用者启用此功能。出于我们未在 NDK 中默认启用此功能的相同原因,您应避免代表使用者做出此选择。

如果您确实需要在公共头文件中使用此行为,请确保记录下来,以便您的用户既知道他们需要启用此功能,又了解这样做的风险。