使用新版 API

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

默认情况下,应用中对 NDK API 的引用属于强引用。当您的库加载后,Android 的动态加载器会迫切地解析它们。如果未找到这些符号,应用将中止。这与 Java 的行为方式相反,在 Java 中,除非调用缺失的 API,否则不会抛出异常。

因此,NDK 会阻止您创建对比应用的 minSdkVersion 更新的 API 的强引用。这可防止您不小心交付在测试期间有效但在旧设备上无法加载(System.loadLibrary() 将抛出 UnsatisfiedLinkError)的代码。另一方面,编写使用 API 版本高于应用的 minSdkVersion 的代码更加困难,因为您必须使用 dlopen()dlsym() 来调用 API,而不是使用普通的函数调用。

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

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

在 build 中启用弱 API 引用

CMake

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

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 工具链文件,系统会自动启用该警告,并在启用此功能时提升为错误。

以下示例代码展示了如何在未启用相应功能的情况下通过 dlopen()dlsym() 有条件地使用 API:

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,最简单的方式是尝试在没有 guard(或 __builtin_available(android 1, *) 的 guard)的情况下进行构建,然后按照错误消息中的指示执行构建。例如,使用 minSdkVersion 24AImageDecoder_createFromAAsset() 进行无保护措施调用时,会生成:

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

在这种情况下,调用应由 __builtin_available(android 30, *) 保护。如果没有构建错误,则表示 API 始终可供 minSdkVersion 使用,无需任何防护;或者 build 配置有误,并且 unguarded-availability 警告处于停用状态。

或者,NDK API 参考文档还会针对每个 API 显示“在 API 30 中引入”之类的内容。如果不存在该文本,则表示该 API 适用于所有受支持的 API 级别。

避免重复使用 API Guard

如果您使用此方法,您的应用中可能会有只能在足够新设备上使用的代码段。您可以将自己的代码注释为需要特定 API 级别,而不是在每个函数中重复执行 __builtin_available() 检查。例如,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 调用的函数,Clang 也会发出该警告。如需避免在自己的代码中重复执行保护程序,请参阅避免重复 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),但这些 API 越新,就越有可能成为很少有开发者需要的极端情况。也就是说,如果您是这些开发者之一,如果您的 minSdkVersion 早于 API,那么现在您需要继续使用 dlsym() 来调用这些 API。这是一个可以解决的问题,但这样做可能会破坏所有应用的源代码兼容性(由于 libc 和本地声明上的 availability 属性不匹配,任何包含 libc API 的 polyfill 的代码都将无法编译),因此我们不确定是否或何时修复此问题。

当包含新 API 的比您的 minSdkVersion 新时,更多开发者可能会遇到这种情况。此功能只能启用弱符号引用;不存在弱库引用这样的内容。例如,如果您的 minSdkVersion 为 24,您可以关联 libvulkan.so 并对 vkBindBufferMemory2 进行受保护的调用,因为 libvulkan.so 在 API 24 及以上的设备上可用。另一方面,如果您的 minSdkVersion 为 23,您必须回退到 dlopendlsym,因为在仅支持 API 23 的设备上设备上不存在该库。我们没有找到合适的解决方案来解决此问题,但从长远来看,它会自行解决,因为我们(如果可能)不再允许新 API 创建新库。

面向图书馆作者

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

如果您确实需要在公开头文件中实现此行为,请务必进行记录,以便您的用户都知道需要启用该功能,并且了解执行此操作的风险。