使用新版 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 中默认不启用此功能,因此您应避免代表使用方做出此选择。

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