使用新版 API

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

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

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

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

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

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

以下示例代码使用 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 使用哪个值,最简单的方法是尝试在不使用守卫(或 __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 使用,且无需任何守卫;或者您的 build 配置错误,并且 unguarded-availability 警告已停用。

或者,对于每个 API,NDK API 参考文档中都会显示“Introduced in API 30”(在 API 30 中引入)之类的文字。如果没有该文本,则表示该 API 适用于所有受支持的 API 级别。

避免重复使用 API 守卫

如果您使用此方法,应用中可能有一些代码段仅适用于足够新的设备。您可以为自己的代码添加注解,指明其需要特定的 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 守卫的怪癖

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() 不可用”错误,而是会出现段错误。使用强引用后,错误消息会更加清晰,并且快速失败是一种更安全的默认设置。

由于这是一项新功能,因此很少编写现有代码来安全地处理此行为。未以 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 中默认不启用此功能,因此您应避免代表使用方做出此选择。

如果您确实需要在公共头文件中实现此行为,请务必记录这一点,以便用户知道他们需要启用此功能,并了解这样做的风险。