本页介绍了您的应用如何在较新的操作系统版本上运行时使用新的操作系统功能,同时保持与旧设备的兼容性。
默认情况下,您的应用对 NDK API 的引用是强引用。加载库时,Android 的动态加载程序会急切地尝试解析这些引用。如果找不到符号,应用将中止。这与 Java 的行为相反,Java 只会在调用缺失的 API 时抛出异常。
因此,NDK 会阻止您创建指向比应用的 minSdkVersion
更新的 API 的强引用。这可以保护您免于意外发布在测试期间工作但在旧设备上无法加载的代码(将从 System.loadLibrary()
抛出 UnsatisfiedLinkError
)。另一方面,编写使用比应用的 minSdkVersion
更新的 API 的代码更加困难,因为您必须使用 dlopen()
和 dlsym()
调用 API,而不是正常的函数调用。
使用强引用的替代方法是使用弱引用。如果在加载库时找不到弱引用,该符号的地址将被设置为 nullptr
,而不是导致库加载失败。它们仍然不能安全调用,但只要对调用站点进行防护以防止在 API 不可用时调用,其余代码就可以运行,并且您无需使用 dlopen()
和 dlsym()
即可正常调用 API。
弱 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 工具链文件,启用此功能时将自动启用该警告并将其升级为错误。
以下是在未启用此功能的情况下,使用 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 24
对 AImageDecoder_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)
进行防护,这甚至阻止了弱声明被看到。由于现代 NDK 支持的最旧的 API 级别是 r21,因此最常用的 libc API 已经可用。每个版本都会添加新的 libc API(请参阅 status.md),但它们越新,就越可能是少数开发者需要的边缘情况。也就是说,如果您是这些开发者之一,目前您需要继续使用 dlsym()
来调用这些 API,如果您的 minSdkVersion
低于该 API。这是一个可以解决的问题,但这样做会带来破坏所有应用的源代码兼容性的风险(任何包含 libc API polyfill 的代码将因 libc 和本地声明上的 availability
属性不匹配而无法编译),因此我们不确定是否或何时会修复它。
更多开发者可能会遇到的情况是,包含新 API 的库比您的 minSdkVersion
更新。此功能仅启用弱符号引用;不存在弱库引用。例如,如果您的 minSdkVersion
是 24,您可以链接 libvulkan.so
并对 vkBindBufferMemory2
进行防护调用,因为 libvulkan.so
在 API 24 及更高版本的设备上可用。另一方面,如果您的 minSdkVersion
是 23,则必须回退到 dlopen
和 dlsym
,因为该库在仅支持 API 23 的设备上不存在。我们不知道解决此问题的良好方案,但从长远来看,它会自行解决,因为我们(只要可能)不再允许新 API 创建新库。
对于库作者
如果您正在开发用于 Android 应用的库,则应避免在公共头文件中使用此功能。它可以在非内联代码中安全使用,但如果您在头文件中的任何代码(例如内联函数或模板定义)中依赖于 __builtin_available
,则会迫使所有使用者启用此功能。出于我们不在 NDK 中默认启用此功能的相同原因,您应该避免代表您的使用者做出该选择。
如果您确实需要在公共头文件中实现此行为,请确保记录下来,以便您的用户既知道需要启用该功能,又了解这样做的风险。